Angular 20 + SSR: Hydration error on dynamic routes (e.g., /news/:category)

I’m using Angular 20 with server-side rendering (Angular SSR). Everything works correctly on most routes, but I encounter a hydration error on dynamic routes like /news/:category or /tools/:id. The error appears in the browser console after hydration:

main.ts:6 ERROR Error: ASSERTION ERROR: Unexpected state: no hydration info available for a given TNode, which represents a view container. [Expected=> null != undefined <=Actual]

If I change the same component to use a static route (e.g., /news/general without a route parameter), the error does not occur and hydration completes without issues.

It seems like Angular is failing to hydrate properly when dynamic route parameters are involved.

What I’ve tried:

Verified that the component renders properly in SSR and client modes.

Ensured that data fetching completes on the server before hydration.

Compared static vs dynamic route behavior — only dynamic routes trigger the error.

Looked through Angular SSR documentation, but couldn’t find a clear solution.

Questions:

What might be causing this hydration error only on dynamic routes?

Are there any examples or best practices for configuring Angular 20 SSR properly with dynamic routes?

Is there a workaround or fix for this issue?

Transforming WebGL2 canvas based on touch events?

I want a webgl canvas that I can seemlessly translate, rotate and scale with multitouch (using touch events).

So far I have a canvas that I can translate with 1 finger or scale with 2 fingers, but I’m having trouble integrating all the functionality into one.

So far I’m completely clueless how to support rotation at all.

My code:

const vertSource = `#version 300 es

in vec4 a_position;
in vec2 a_texcoord;

uniform mat4 u_matrix;

out vec2 v_texcoord;

void main() {
  gl_Position = u_matrix * a_position;
  
  v_texcoord = a_texcoord;
}`
const fragSource = `#version 300 es

precision highp float;

in vec2 v_texcoord;

uniform sampler2D u_texture;

out vec4 outColor;

void main() {
  outColor = texture(u_texture, v_texcoord);
}`
const css = `html, body, canvas {
  width: 100%;
  height: 100%;
}
body {
  overscroll-behavior-y: contain;
  overflow: hidden;
  touch-action: none;
}
*,
*::before,
*::after {
  box-sizing: border-box;
  margin: 0;
}`

function define(target, defines) {
  return Object.defineProperties(target, Object.getOwnPropertyDescriptors(defines))
}

function vec2(x, y) {
  if (x === undefined)
    x = 0
  if (y === undefined)
    y = x
  
  return define([x, y], {
    get x() { return this[0] },
    set x(value) { this[0] = value },
    get y() { return this[1] },
    set y(value) { this[1] = value },
  })
}

const mat4 = {
  translation(x, y, z = 0) {
    return [
      1, 0, 0, 0,
      0, 1, 0, 0,
      0, 0, 1, 0,
      x, y, z, 1,
    ]
  },
  xRotation(angle) {
    const c = Math.cos(angle)
    const s = Math.sin(angle)
    
    return [
      1,  0, 0, 0,
      0,  c, s, 0,
      0, -s, c, 0,
      0,  0, 0, 1,
    ]
  },
  yRotation(angle) {
    const c = Math.cos(angle)
    const s = Math.sin(angle)
    
    return [
      c, 0, -s, 0,
      0, 1,  0, 0,
      s, 0,  c, 0,
      0, 0,  0, 1,
    ]
  },
  zRotation(angle) {
    const c = Math.cos(angle)
    const s = Math.sin(angle)
    
    return [
       c, s, 0, 0,
      -s, c, 0, 0,
       0, 0, 1, 0,
       0, 0, 0, 1,
    ]
  },
  scale(x, y, z = 1) {
    if (y === undefined)
      y = x
    return [
      x, 0, 0, 0,
      0, y, 0, 0,
      0, 0, z, 0,
      0, 0, 0, 1,
    ]
  },
  projection(width, height, depth) {
    // Note: This matrix flips the Y axis so 0 is at the top.
    return [
       2 / width, 0, 0, 0,
       0, -2 / height, 0, 0,
       0, 0, 2 / depth, 0,
      -1, 1, 0, 1,
    ]
  },
  orthographic(left, right, bottom, top, near, far) {
    return [
      2 / (right - left), 0, 0, 0,
      0, 2 / (top - bottom), 0, 0,
      0, 0, 2 / (near - far), 0,
      
      (left + right) / (left - right),
      (bottom + top) / (bottom - top),
      (near + far) / (near - far),
      1,
    ]
  },
  mul(a, b) {
    const b00 = b[0 * 4 + 0]
    const b01 = b[0 * 4 + 1]
    const b02 = b[0 * 4 + 2]
    const b03 = b[0 * 4 + 3]
    const b10 = b[1 * 4 + 0]
    const b11 = b[1 * 4 + 1]
    const b12 = b[1 * 4 + 2]
    const b13 = b[1 * 4 + 3]
    const b20 = b[2 * 4 + 0]
    const b21 = b[2 * 4 + 1]
    const b22 = b[2 * 4 + 2]
    const b23 = b[2 * 4 + 3]
    const b30 = b[3 * 4 + 0]
    const b31 = b[3 * 4 + 1]
    const b32 = b[3 * 4 + 2]
    const b33 = b[3 * 4 + 3]
    const a00 = a[0 * 4 + 0]
    const a01 = a[0 * 4 + 1]
    const a02 = a[0 * 4 + 2]
    const a03 = a[0 * 4 + 3]
    const a10 = a[1 * 4 + 0]
    const a11 = a[1 * 4 + 1]
    const a12 = a[1 * 4 + 2]
    const a13 = a[1 * 4 + 3]
    const a20 = a[2 * 4 + 0]
    const a21 = a[2 * 4 + 1]
    const a22 = a[2 * 4 + 2]
    const a23 = a[2 * 4 + 3]
    const a30 = a[3 * 4 + 0]
    const a31 = a[3 * 4 + 1]
    const a32 = a[3 * 4 + 2]
    const a33 = a[3 * 4 + 3]
    
    return [
      b00 * a00 + b01 * a10 + b02 * a20 + b03 * a30,
      b00 * a01 + b01 * a11 + b02 * a21 + b03 * a31,
      b00 * a02 + b01 * a12 + b02 * a22 + b03 * a32,
      b00 * a03 + b01 * a13 + b02 * a23 + b03 * a33,
      b10 * a00 + b11 * a10 + b12 * a20 + b13 * a30,
      b10 * a01 + b11 * a11 + b12 * a21 + b13 * a31,
      b10 * a02 + b11 * a12 + b12 * a22 + b13 * a32,
      b10 * a03 + b11 * a13 + b12 * a23 + b13 * a33,
      b20 * a00 + b21 * a10 + b22 * a20 + b23 * a30,
      b20 * a01 + b21 * a11 + b22 * a21 + b23 * a31,
      b20 * a02 + b21 * a12 + b22 * a22 + b23 * a32,
      b20 * a03 + b21 * a13 + b22 * a23 + b23 * a33,
      b30 * a00 + b31 * a10 + b32 * a20 + b33 * a30,
      b30 * a01 + b31 * a11 + b32 * a21 + b33 * a31,
      b30 * a02 + b31 * a12 + b32 * a22 + b33 * a32,
      b30 * a03 + b31 * a13 + b32 * a23 + b33 * a33,
    ]
  },
}

const camera = vec2()
let rotation = 0
let scaling = 1
const tiles = [
  [0, 0],
  [1, 0],
  [2, 0],
  [3, 0],
  [4, 0],
  [5, 0],
  [6, 0],
  [0, 1],
  [0, 2],
  [0, 3],
  [0, 4],
  [1, 4],
  [2, 4],
  [3, 4],
  [4, 4],
  [5, 4],
  [6, 4],
  [6, 1],
  [6, 2],
  [6, 3],
]

function resizeCanvasToDisplaySize(canvas, mult = 1) {
  const width = canvas.clientWidth * mult | 0
  const height = canvas.clientHeight * mult | 0
  if (canvas.width != width || canvas.height != height) {
    canvas.width  = width
    canvas.height = height
    return true
  }
  return false
}

function createShader(type, source) {
  const shader = gl.createShader(type)
  gl.shaderSource(shader, source)
  gl.compileShader(shader)
  const success = gl.getShaderParameter(shader, gl.COMPILE_STATUS)
  if (success)
    return shader
  
  console.log(gl.getShaderInfoLog(shader))
  gl.deleteShader(shader)
  return undefined
}

function createProgram(vertexShader, fragmentShader) {
  const program = gl.createProgram()
  gl.attachShader(program, vertexShader)
  gl.attachShader(program, fragmentShader)
  gl.linkProgram(program)
  const success = gl.getProgramParameter(program, gl.LINK_STATUS)
  if (success)
    return program

  console.log(gl.getProgramInfoLog(program))
  gl.deleteProgram(program)
  return undefined
}

function setupTouchEvents() {
  let cam, p0, p1
  let rot, scale
  gl.canvas.ontouchstart = e => {
    const t0 = e.touches[0]
    const t1 = e.touches[1]
    const [x0, y0] = [t0.clientX, t0.clientY]
    const [x1, y1] = [t1?.clientX, t1?.clientY]
    
    if (e.touches.length == 1) {
      p0 = vec2(x0, y0)
      cam = vec2(camera.x, camera.y)
    }
    else {
      p0 = vec2(x0, y0)
      p1 = vec2(x1, y1)
      cam = vec2(camera.x, camera.y)
      scale = Math.hypot(p0.x - p1.x, p0.y - p1.y)
    }
  }
  gl.canvas.ontouchmove = e => {
    const t0 = e.touches[0]
    const t1 = e.touches[1]
    const [x0, y0] = [t0.clientX, t0.clientY]
    const [x1, y1] = [t1?.clientX, t1?.clientY]
    
    if (e.touches.length == 1) {
      camera.x = cam.x + (x0 - p0.x) / 32
      camera.y = cam.y + (y0 - p0.y) / 32
    }
    else {
      p0 = vec2(x0, y0)
      p1 = vec2(x1, y1)
      
      const newScale = Math.hypot(p0.x - p1.x, p0.y - p1.y)
      scaling = newScale / scale
      
      camera.x = cam.x + (x0 - p0.x) / 32
      camera.y = cam.y + (y0 - p0.y) / 32
    }
  }
  gl.canvas.ontouchend = e => {
    const t = e.changedTouches[0]
    const [x, y] = [t.clientX, t.clientY]
    
    
    if (t.identifier == 0) {
      
    }
  }
}

function main() {
  const style = document.createElement('style')
  style.textContent = css
  document.head.append(style)
  
  const canvas = document.createElement('canvas')
  document.body.append(canvas)
  
  define(globalThis, { gl: canvas.getContext('webgl2') })
  if (!gl)
    return
  
  // Create shader program
  const vertexShader = createShader(gl.VERTEX_SHADER, vertSource)
  const fragmentShader = createShader(gl.FRAGMENT_SHADER, fragSource)
  const program = createProgram(vertexShader, fragmentShader)
  
  // Find attribute and uniform locations
  const a_position = gl.getAttribLocation(program, 'a_position')
  const a_texcoord = gl.getAttribLocation(program, 'a_texcoord')
  const u_matrix = gl.getUniformLocation(program, 'u_matrix')

  // Create a vertex array object (attribute state)
  const vao = gl.createVertexArray()
  gl.bindVertexArray(vao)

  // Create position buffer
  const positionBuffer = gl.createBuffer()
  gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer)
  const positions = [
    0, 0, 0,
    0, 1, 0,
    1, 0, 0,
    1, 1, 0,
  ]
  gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW)

  // Turn on the attribute
  gl.enableVertexAttribArray(a_position)
  gl.vertexAttribPointer(a_position, 3, gl.FLOAT, false, 0, 0)

  // Create texcoord buffer
  const texcoordBuffer = gl.createBuffer()
  gl.bindBuffer(gl.ARRAY_BUFFER, texcoordBuffer)
  const texcoords = [
    0, 0,
    0, 1,
    1, 0,
    1, 1,
  ]
  gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(texcoords), gl.STATIC_DRAW)

  // Turn on the attribute
  gl.enableVertexAttribArray(a_texcoord)
  gl.vertexAttribPointer(a_texcoord, 2, gl.FLOAT, true, 0, 0)

  // Create a texture
  const texture = gl.createTexture()
  gl.activeTexture(gl.TEXTURE0 + 0)
  gl.bindTexture(gl.TEXTURE_2D, texture)
  gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 2, 2, 0, gl.RGBA, gl.UNSIGNED_BYTE, new Uint8Array([
    0, 0, 255, 255,
    255, 0, 0, 255,
    255, 0, 0, 255,
    0, 0, 255, 255,
  ]))
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST)
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST)
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE)
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE)
  
  // Create index buffer
  const indexBuffer = gl.createBuffer()
  gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer)
  const indices = [
    0, 1, 2,
    2, 1, 3,
  ]
  gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(indices), gl.STATIC_DRAW)
  
  setupTouchEvents()

  let lastTime = 0
  function drawScene(time) {
    // Subtract the previous time from the current time
    const delta = (time - lastTime) / 1000
    // Remember the current time for the next frame.
    lastTime = time
    
    resizeCanvasToDisplaySize(gl.canvas)
    gl.viewport(0, 0, gl.canvas.width, gl.canvas.height)
    
    // Turn on depth testing and culling
    gl.enable(gl.DEPTH_TEST)
    gl.enable(gl.CULL_FACE)
    
    // Clear color and depth information
    gl.clearColor(0, 0, 0, 1)
    gl.clearDepth(1)
    gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT)
    
    // Bind the attribute/buffer set we want
    gl.useProgram(program)
    gl.bindVertexArray(vao)
    
    const unit = 32
    let proj = mat4.projection(gl.canvas.clientWidth / unit, gl.canvas.clientHeight / unit, 400)
    proj = mat4.mul(proj, mat4.translation(camera.x, camera.y))
    proj = mat4.mul(proj, mat4.zRotation(rotation))
    proj = mat4.mul(proj, mat4.scale(scaling))
    
    tiles.forEach(([x, y]) => {
      // Transform matrix
      const matrix = mat4.mul(proj, mat4.translation(x, y))
      
      // Set the matrix
      gl.uniformMatrix4fv(u_matrix, false, matrix)
      
      // Draw the geometry
      gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0)
    })
    
    // Call drawScene again next frame
    requestAnimationFrame(drawScene)
  }
  requestAnimationFrame(drawScene)
}

main()

Regex for age validation in JavaScript

I have the following input in my index.html, which asks the user to enter their age

 <input type="text" placeholder="Enter your age:" id="age" />

I would like to check the input for valid values, using a regular expression. Even though the input is of type:text i want it to accept only values from 0-99 (considering the normal age of human living).
Is the following regex valid /s{0-9}{0-9} for my approach?

How to use getBoundingClientRect() considering Windows screen scale factor?

I’m using the code below to get the position of a specific element on screen :

document.getElementById('my-element').getBoundingClientRect();

For instance, it will return :

DOMRect {x: 1406, y: 356.3999938964844, width: 52, height: 52, top: 356.3999938964844, …}

On Windows 11 with Chrome, it works correctly as long as the Scale on System > Display is set to 100%. But usually on high res monitors, it’s always set bigger, like the image below. In this scenario, the coordinates returned by the command above won’t reflect the correct position of the element. How can i make the getBoundingClientRect() command work properly in any display scale on Windows 11 ?

enter image description here

How to synchronize hover time and seek time in a custom React video player using ReactPlayer?

I’m building a custom video player in React and I’m having trouble aligning the time displayed on hover over the progress bar with the actual seek time used when clicking.

Problem
I use an as the progress bar.

On onMouseMove, I calculate the percentage based on event.clientX and show a tooltip (indicadorRef) displaying the estimated time for that position.

The time displayed on hover looks visually correct.

However, when the user clicks (using onChange), the actual seek time does not exactly match the hovered time — usually off by a small fraction of a second.

I’ve tried rounding values and aligning steps, but small mismatches remain.

Example:

const handleHoverProgress = (event) => {
  if (!isPlayerReady) return;

  const rect = inputProgressRef.current.getBoundingClientRect();
  const x = event.clientX - rect.left;
  const percentage = Math.max(0, Math.min(1, x / rect.width));

  const min = parseFloat(inputProgressRef.current.min);
  const max = parseFloat(inputProgressRef.current.max);
  const step = parseFloat(inputProgressRef.current.step) || 0.001;

  let value = min + percentage * (max - min);
  value = Math.round(value / step) * step;
  value = parseFloat(value);

  indicadorRef.current.textContent = formatTime(value * duration);
  indicadorRef.current.style.visibility = "visible";

  let indicatorX = x - indicadorRef.current.offsetWidth / 2;
  indicatorX = Math.max(0, Math.min(rect.width - indicadorRef.current.offsetWidth, indicatorX));
  indicadorRef.current.style.left = `${indicatorX}px`;
};

ReactPlayer setup:

<ReactPlayer
  ref={playerRef}
  url={currentVideoUrl}
  playing={playing}
  volume={volume}
  onProgress={({ played }) => setPlayed(played)}
  onDuration={setDuration}
  onReady={() => setIsPlayerReady(true)}
  controls={false}
  width="100%"
  height="100%"
  progressInterval={100}
/>

Range input:

<input
  ref={inputProgressRef}
  onMouseMove={handleHoverProgress}
  type="range"
  min={0}
  max={1}
  step={0.001}
  value={played}
  onChange={(e) => handleSeek(parseFloat(e.target.value))}
  className="progress-bar"
/>

What I’ve tried
Matching the tooltip value with the seek value by using the same percentage calculation.

Rounding to the nearest step to avoid floating-point issues (e.g., 0.73899999).

Using progressInterval={100} on ReactPlayer for more frequent updates.

The issue
Despite all this, the hovered time and the actual seek time don’t perfectly match after clicking. Sometimes it’s off by a small fraction of a second or by one second depending on rounding behavior.

What I’m looking for
How can I ensure that the time shown on hover exactly matches the time that the video seeks to when the user clicks?

Technologies
React

ReactPlayer

Any help or suggestions are appreciated. Thank you!

enter image description here

Nginx Showing 404 in Header Despite Page Rendering Fine (CentOS, aaPanel, CodeIgniter 4)

I’m experiencing a strange issue where the page at
https://customsportswears.com/ibc52/custom-afl-uniforms-high-quality-australian-football-league-gear.html
loads fine in the browser, but when checked via developer tools or external services, the HTTP response header is 404.

Stack:

  • CentOS (aaPanel)
  • Nginx 1.24.0
  • PHP 8.3 (via PHP-FPM)
  • CodeIgniter 4

Current Nginx Configuration (Partial):

error_page 403 500 502 503 504 /index.php;
error_page 404 /404.php;

location = /404.php {
    internal;
    fastcgi_pass 127.0.0.1:9000;
    fastcgi_index 404.php;
    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    include fastcgi_params;
}

location / {
    try_files $uri $uri/ /index.php$is_args$args;
    add_header Access-Control-Allow-Origin "*";
    add_header Access-Control-Allow-Methods "GET, POST, OPTIONS";
    add_header Access-Control-Allow-Headers "Content-Type, Authorization";
}

location ~ .php$ {
    include fastcgi_params;
    fastcgi_pass 127.0.0.1:9000;
    fastcgi_index index.php;
    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
} ```

What's Happening:
- The actual rendered page appears correct.
- However, HTTP response header for that URL is returning a 404 status.
- Suspect CodeIgniter or nginx config may be causing this, possibly due to try_files, error_page logic, or internal redirect conflicts.
Tried:
- Confirmed that the controller and route in CI4 return content properly.
- Double-checked the file path and route configs.
- Response status mismatch suggests a misconfiguration or forced status return.
Looking for insights from anyone who's handled similar 404 header issues with CodeIgniter and nginx under aaPanel. Could it be a CI4 header override after routing logic? Or does nginx misinterpret the fallback URL?
Any help is appreciated!

Create a fake random by changing a string into a number in PHP [closed]

I would like a way to show a random image on my site, but not random each time people refresh the page, but random for each string,
For this I have differents css rules :

.random1, .random2, .random3, …, .random10

Actually I do this:

<div class="random<?= rand(1, 10 ?>">...</div>

But I don’t want this to change at each refresh, so I would like to change for example the 4 first characters of my string (the content of the div) into a number
For example, “Hello” would return “3”, or “In this article …” would return 7.

But I don’t know how to do this ?
Thanks

display image from laravel application public folder path to jquery data table

I am trying to render image from public folder to the DataTable, where the image information like path are stored in database.
When I hard code the image information, the image is rendered. But when I make it dynamic it does not appear.
I took the reference from Display images from Database in Datatables using jQuery for a Laravel app
but it’s not working for me.

My ajax code,

var table = $('.data-table').DataTable({
            processing: true,
            serverSide: true,
            ajax: "{{ route('students.index') }}",
            columns: [{
                    data: 'DT_RowIndex',
                    name: 'DT_RowIndex'
                },
                {
                    data: 'name',
                    name: 'name'
                },
                {
                    data: 'image',
                    name: 'image',
                    "render": function (data) {
                    return '<img src="{{ asset("images/' +data+'") }}" />' }
                },
                {
                    data: 'action',
                    name: 'action',
                    orderable: false,
                    searchable: false
                },
            ]
        });

In browser, when I inspect the img tag it shows::

<img src="http://127.0.0.1:8000/images/' +data+'">

When I hard code the image name, it’s showing the image.

{
                    data: 'image',
                    name: 'image',
                    "render": function (data) {
                    return '<img src="{{ asset("images/9867543cu4R.jpg") }}" />' }
                },

How to solve this issue, please help.

Doctrine not saved relation when saving child entity with setting parent entity object

Here is example of mapping:

Cateogory mapping:

<entity name="AppCoreDomainPostItem" table="post_item">
   <id name="id" type="integer" column="id">
       <generator strategy="SEQUENCE"/>
   </id>
   <field name="title" type="string" length="256" />
   <many-to-one field="category"
           target-entity="AppCoreDomainPostCategory"
           inversed-by="items"
    >
       <join-column name="category_id" />
    </many-to-one>
</entity>

Item mapping:

<entity name="AppCoreDomainPostCategory" table="post_category">
   <id name="id" type="integer" column="id">
       <generator strategy="SEQUENCE"/>
   </id>
   <field name="title" type="string" length="256" />
   <one-to-many
           field="items"
           target-entity="AppCoreDomainPostItem"
           mapped-by="category"
           fetch="EXTRA_LAZY"
        >
        <cascade><cascade-all/></cascade>
    </one-to-many>
</entity>

Im trying to save item entity with category entity:

<?php
//...
$category = $em->getRepository(Category::class)->find($categoryId);
//category exists and received as Category object
$item = new Item();
$item->setTitle('test')
$item->setCategory($category);
$em->persist($item);
$em->flush();

Getting error:

A new entity was found through the relationship 'AppCoreDomainPostItem#category' that was   
  not configured to cascade persist operations for entity: TestCategory. To solve this issue: Either explicitly call Enti  
  tyManager#persist() on this unknown entity or configure cascade persist this association in the mapping for exa  
  mple @ManyToOne(..,cascade={"persist"})

If I trying with category reference:

$category = $em->getReFerence(Category::class, $categoryId);
$item = new Item();
$item->setTitle('test')
$item->setCategory($category);
$em->persist($item);
$em->flush();

Record in database saved, but category_id field is null

What am i doing wrong?

In Javascript/Typescript there’s a pattern for checks his own class? Or did I come up with something new?

Consider the following code:

import type { Account } from "@/models/account";
import { fromAccountToFirebase, fromFirebaseToAccount } from "@/scripts/common/map-firestore-account";
import { ImplFirestoreStorage } from "@/services/impl/impl-firestore-storage";
import type { StorageService } from "@/services/interfaces/storage-service";
import type { User } from "firebase/auth";
import { limit, where, type DocumentData, type Firestore } from "firebase/firestore/lite";
import { filter, from, map, type Observable } from "rxjs";

/** from storage-service.ts */

// base for any data mapped to an id.
export interface SupportsId<ID> {
    readonly id: ID;
}

export interface StorageService<T extends SupportsId<ID>, ID> {
    /** gets the first value with the matching id, if no value is found becomes empty. */
    getById(id: ID): Observable<T>;
    /** gets all matches that pass the filter for the max amount of entries to find, an optional
     * start index will skip the first entries.
     */
    getFiltered(filter: (value: T) => boolean, maxCount: number, startIdx?: number): Observable<T[]>;
    /** write the value into the database and raises an error if write fails. */
    update(value: T): Observable<void>;
}

/** from impl-firestore-storage.ts */

export abstract class ImplFirestoreStorage<T extends SupportsId<string>> implements StorageService<T, string> {
    // implementation for the required methods.

    public getByQuery(...constraints: QueryConstraint[]): Observable<T[]> { /** ... */ }
}

/** from impl-sql-storage.ts */

export abstract class ImplSqlStorage<T extends SupportsId<string>> implements StorageService<T, string> {
    // implementation for the required methods.
}

/** from account-storage-service.ts */

export class AccountsStorageService extends ImplFirestoreStorage<Account> {

    /** get the account data from the matching user registered with the account. */
    public getByUser(user: User): Observable<Account> {

        if (this instanceof ImplFirestoreStorage) {
            // use more optimized query if using firestore.
            return from(this.getByQuery(limit(1), where("userId", "==", user.uid))).pipe(
                filter(array => array.length !== 0),
                map(array => array[0]));
        }

        // fallback with the generic query method.
        return (this as StorageService<Account, string>).getFiltered(account => account.userId === user.uid, 1).pipe(
            filter(array => array.length !== 0),
            map(array => array[0]));
    }
}

In this code there’s a common database signature called StorageService that ensure the presence of some method, then the AccountsStorageService is a global class to interact with a database of accounts.

In the code base I have 2 implementations to interact with a firestore database and a sql database. Why I have like this? In this project we are switching between using either a firestore or classic sql database and managed to make an implementation for both. Don’t ask further about this.

But here an interesting thing, the firestore has a query system for more optimised requests and the final class AccountsStorageService checks if itself is extended either from the firestore implementation to use that more optimised system or if not use the generic one.

I find this interesting even if I’ve never seen such pattern anywhere, I come up with this thing. I had to use that casting to fight againts typescript that flagged after the if the instance as never.
I imagine at compile/bundle time the script will be optimized to use the content of the if directly.

Even if this question looks like a code review, and I know those aren’t allowed in this website, I actually want to know if this is a code patter that exists. That’s the question.
This thing I wrote works perfectly!

Javascript formatter that allows excluding sections?

I’m looking for a JavaScript formatter that allows skipping sections. I’m not too picky about the formatting style, but not being able to exclude a section is a deal-breaker, so Prettier is out.

Example of a section I want to exclude from formatting:

    class X {
        ...

        // stop-formatting
        get lines() { return this.#lines.length                  }
        get col()   { return this.#x + 1                         }
        get row()   { return this.#y + 1                         }
        get done()  { return this.#y >= this.#lines.length       }
        get eol()   { return this.#x >= this.current_line.length }
        // resume-formatting
    }

How could I log the height of a dynamically created element?

I am working on app that lets users manage a list of contacts. I have a function for a mobile view that creates a list of elements that displays contact information. Each of these elements have additional elements appended to them so as to show a contacts picture, name, email address, organization and role that they are associated with. The contact’s organization and role are displayed in a p tag together. This p tag is assigned to a variable named contactListOrganizationRoleAndElement. I noticed that sometimes when viewing the contact list elements in a mobile setting these p tags increase their height so as to accommodate long text and that causes them to display some of the text on a new line and this doesn’t look good. I figured that in the function that I am using I could detect whenever a p tag exceeded 18px in height and adjust the second text to be an ellipsis to indicate to the user that the text was too long to fit and also maintain a consistent appearance for these contact elements. I am able to log the p tag’s via a variable called contactListOrganizationAndRoleElement to my console, and when I hover over the logged elements in the console a tooltip appears giving the dimension of the elements. However, when I try to log the height of these p tags to the console it either gives me a blank line or a value of 0. I found this strange because I managed to log further style information about the p tags via the contactListOrganizationAndRoleElement variable such as its fontWeight and margin properties. I figured then that I should also be able to log the height from the variable’s style property but the height property from style returns a blank line. I also tried using clientHeight and offSetHeight properties from the variable but these are returning a value of 0. How might I log the height of the variable contactListOrganizationAndRoleElement so as to know the height of these p tags?

enter image description here

async function renderMobileContactsListContent() {
     const userId = sessionStorage.getItem("user");
    const user = await getUser(userId);
    const userContacts = await getUserContacts(userId)

    const contactsListUserImage = document.querySelector("#contacts-user-image");
    if (user.user_image !== null && user.user_image !== './images/user-5-svgrepo-com.svg') {
        contactsListUserImage.setAttribute("src", user.user_image);
        contactsListUserImage.style.borderRadius = "50%";
    }

    const contactsUserHeaderNameContainer = document.querySelector("#contacts-user-header-name-container");
    // contactsUserHeaderNameContainer.style.margin = "0px 0px 0px 10px"
    const contactsHeaderUserNameElement = document.querySelector("#mobile-contact-header-user-name");
    contactsHeaderUserNameElement.style.margin = "0px";
    const contactsHeaderUserEmailAddressElement = document.querySelector("#contact-header-user-email")
    // contactsHeaderUserEmailAddressElement.style.margin = "0px 0px 16px 0px";

    const contactsUserNameElement = document.querySelector("#contacts-user-name");
    contactsUserNameElement.style.margin = "0px";
    const contactsUserEmailAddressElement = document.querySelector("#contacts-user-email");
    contactsUserEmailAddressElement.style.margin = "0px";
    contactsUserNameElement.innerHTML = `${user.firstname}'s Contacts`;
    // contactsUserNameElement.style.fontSize = "xx-large"
    contactsUserNameElement.style.fontFamily = "Arial"
    // contactsUserEmailAddressElement.innerHTML = `${user.emailaddress}`

    contactsHeaderUserNameElement.innerHTML = `${user.firstname} ${user.lastname}`;
    contactsHeaderUserEmailAddressElement.innerHTML = user.emailaddress;

    console.log(userContacts)

    userContacts.sort(function(a, b) {
        if (a.firstname < b.firstname) {
            return -1;
        }
        if (a.firstname < b.firstname) {
            return 1;
        }
        
        var aFirstChar = a.firstname.charAt(0);
        var bFirstChar = b.firstname.charAt(0);
        if (aFirstChar > bFirstChar) {
          return 1;
        } else if (aFirstChar < bFirstChar) {
          return -1;
        } else {
          var aLastChar = a.lastname.charAt(0);
          var bLastChar = b.lastname.charAt(0);
          if (aLastChar === "") {
            aLastChar = "z"
          }
          if (bLastChar === "") {
            bLastChar = "z"
          }
          if (aLastChar > bLastChar) {
            return 1;
          } else if (aLastChar < bLastChar) {
            return -1;
          } else {
            return 0;
          }    
        }
      });

    const contactsListContainer = document.createElement("div");
    const contactsHeaderContainer = document.createElement("div");
    contactsHeaderContainer.style.display = "flex";
    // contactsHeaderContainer.style.justifyContent = "space-between"
    contactsHeaderContainer.style.alignItems = "center"
    // contactsHeaderContainer.style.width = "25%";
    contactsHeaderContainer.style.backgroundColor = "ghostwhite"
    // contactsHeaderContainer.style.marginBottom = "5px"
    contactsHeaderContainer.style.padding = "5px"
    const myContactsHeaderElement = document.createElement("h2");
    myContactsHeaderElement.innerHTML = "My Contacts"
    myContactsHeaderElement.style.width = "140px"
    myContactsHeaderElement.style.margin = "0"
    myContactsHeaderElement.style.marginLeft = "5px"
    // myContactsHeaderElement.style.marginRight = "10px"
    const numberOfContactsElement = document.createElement("h2");
    numberOfContactsElement.innerHTML = userContacts.length;
    numberOfContactsElement.style.display = "inline-flex";
    numberOfContactsElement.style.justifyContent = "center";
    numberOfContactsElement.style.alignItems = "center";
    numberOfContactsElement.style.width = "15px";
    numberOfContactsElement.style.height = "15px";
    numberOfContactsElement.style.backgroundColor = "navy";
    numberOfContactsElement.style.color = "white"
    numberOfContactsElement.style.padding = "10px";
    numberOfContactsElement.style.borderRadius = "50%";
    numberOfContactsElement.style.margin = "0"
    contactsListContainer.style.position = "absolute";
    contactsListContainer.style.top = "28.2%"
    contactsListContainer.style.width = "100%"
    const contactsList = document.createElement("ul");
    contactsList.style.listStyle = "none";
    contactsList.style.padding = "0"
    contactsList.style.margin = "0"
    // contactsList.style.height = "100%";
    // contactsList.style.overflow = "auto"
    // contactsList.style.zIndex = "5"
    userContacts.forEach(contact => {
        const contactListItem = document.createElement("div");
        contactListItem.style.display = "flex";
        contactListItem.style.flexDirection = "row";
        contactListItem.style.height = "100px"
        contactListItem.style.borderTop = "1px solid gray";
        contactListItem.style.borderBottom = "1px solid gray";
        contactListItem.style.backgroundColor = "aliceblue"
        contactListItem.style.marginTop = "1px";
        contactListItem.style.marginBottom = "1px";
        contactListItem.setAttribute("contactId", contact.contact_id)

        contactListItem.addEventListener("mouseover", function() {
            contactListItem.style.backgroundColor = "lightgrey";
        });

        contactListItem.addEventListener("mouseout", function() {
            contactListItem.style.backgroundColor = "aliceblue";
        });

        contactListItem.addEventListener("click", function(event) {
            
                // contactListItem.style.backgroundColor = "green";
                
                function saveDataToURL(url, data) {
                    const urlObject = new URL(url);
                    const params = new URLSearchParams(urlObject.search);
                
                    for (const key in data) {
                        if (data.hasOwnProperty(key)) {
                            params.set(key, data[key]);
                        }
                    }
                    urlObject.search = params.toString();
                    return urlObject.toString();
                }
                
                const myURL = `${rootUrl}/contact_${contact.contact_id}`;
                const myData = {
                    name: `${contact.firstname} ${contact.lastname}`,
                    // age: 30,
                    // city: "New York"
                };
                
                const newURL = saveDataToURL(myURL, myData);
                console.log(newURL);
                // Expected output: "https://example.com/page?name=John+Doe&age=30&city=New+York"
                window.location.href = newURL
        })

        const contactListItemImageContainer = document.createElement("div");
        contactListItemImageContainer.style.display = "flex";
        contactListItemImageContainer.style.alignItems = "center";
        // contactListItemImageContainer.style.width = "10%"
        contactListItemImageContainer.style.padding = "10px"
        const contactListItemImage = document.createElement("img");
        contactListItemImage.style.width = "90px";
        contactListItemImage.style.height = "100%";
        contactListItemImage.style.border = "0.5px solid grey";
        contactListItemImage.style.borderRadius = "50%"
        contactListItemImage.style.backgroundColor = "gainsboro";
        contactListItemImage.style.objectFit = "cover";
        
        if (contact.contact_image !== null && contact.contact_image !== "./images/user-2-svgrepo-com.svg") {
            contactListItemImage.setAttribute("src", contact.contact_image);
        } else {
            contactListItemImage.setAttribute("src", "./images/user-2-svgrepo-com.svg");
        }

        // contactListItem.innerHTML = `${contact.firstname} ${contact.lastname}`;
        const contactListNameContainer = document.createElement("div");
        contactListNameContainer.style.position = "relative";
        contactListNameContainer.style.display = "flex";
        contactListNameContainer.style.flexDirection = "column"
        contactListNameContainer.style.justifyContent = "center";
        contactListNameContainer.style.alignItems = "center";
        contactListNameContainer.style.width = "100%"
        const contactListNameElement = document.createElement("h3");
        contactListNameElement.style.margin = "0";
        contactListNameElement.innerHTML = `${contact.firstname} ${contact.lastname}`;
        const contactListEmailElement = document.createElement("p");
        contactListEmailElement.style.fontStyle = "italic"
        contactListEmailElement.style.margin = "0"

        
        console.log(contact)
        
        if (contact.emailaddress !== null && contact.emailaddress !== "") {
            contactListEmailElement.innerHTML = contact.emailaddress;
        } else {
            contactListEmailElement.innerHTML = "text";
            contactListEmailElement.style.visibility = "hidden";
        }

        const contactListOrganizationAndRoleElement = document.createElement("p");
        contactListOrganizationAndRoleElement.style.fontWeight = "bolder";
        contactListOrganizationAndRoleElement.style.margin = "0";
    
        if (contact.organization !== null && contact.organization !== "" && contact.organization_role !== null && contact.organization_role !== "") {
            contactListOrganizationAndRoleElement.innerHTML = `${contact.organization} || ${contact.organization_role}`
        } else if (contact.organization !== null && contact.organization !== "" || contact.organization_role === null && contact.organization_role === "") {
            contactListOrganizationAndRoleElement.innerHTML = `${contact.organization}`
        } else if (contact.organization === null && contact.organization === "" || contact.organization_role !== null && contact.organization_role !== "") {
            contactListOrganizationAndRoleElement.innerHTML = `${contact.organization_role}`
        } else {
            contactListOrganizationAndRoleElement.innerHTML = "text"
            contactListOrganizationAndRoleElement.style.visibility = "hidden"
        }

        //This is where my issue is.

        //These work...
        console.log(contactListOrganizationAndRoleElement)
        console.log(contactListOrganizationAndRoleElement.style.fontWeight)
        console.log(contactListOrganizationAndRoleElement.style.margin)
        //None of these work...
        console.log(contactListOrganizationAndRoleElement.style.height)
        console.log(contactListOrganizationAndRoleElement.clientHeight)
        console.log(contactListOrganizationAndRoleElement.offsetHeight)

        const contactListFavoritesStarIconContainer = document.createElement("div");
        contactListFavoritesStarIconContainer.style.display = "flex";
        contactListFavoritesStarIconContainer.style.justifyContent = "center";
        contactListFavoritesStarIconContainer.style.alignItems = "center"
        // contactListFavoritesStarIconContainer.style.width = "10%";
        contactListFavoritesStarIconContainer.style.padding = "10px"
        const contactListFavoriteStarImg = document.createElement("img");
        contactListFavoriteStarImg.classList.add("contact-favorite-icon")
        contactListFavoriteStarImg.style.width = "60px"

        console.log(contact.favorite)

        // contactListFavoriteStarImg.addEventListener("click", function(event) {
        //     // event.preventDefault()
        //     updateContactFavorite()
        // }, false)
        
        contactListFavoriteStarImg.setAttribute("src", "./images/star-gold-svgrepo-com.svg");
        if (contact.favorite === null || contact.favorite === false) {
            contactListFavoriteStarImg.style.visibility = "hidden"
        } else {
            contactListFavoriteStarImg.style.display = "block"
        }
        
        contactListItemImageContainer.appendChild(contactListItemImage);
        contactListNameContainer.appendChild(contactListNameElement);
        contactListNameContainer.appendChild(contactListEmailElement);
        contactListNameContainer.appendChild(contactListOrganizationAndRoleElement)
        contactListItem.appendChild(contactListItemImageContainer)
        contactListItem.appendChild(contactListNameContainer)
        contactListFavoritesStarIconContainer.appendChild(contactListFavoriteStarImg);
        contactListItem.appendChild(contactListFavoritesStarIconContainer);
        contactsList.appendChild(contactListItem)
    });
    contactsHeaderContainer.append(myContactsHeaderElement)
    contactsHeaderContainer.appendChild(numberOfContactsElement)
    contactsListContainer.appendChild(contactsHeaderContainer)
    contactsListContainer.appendChild(contactsList)
    document.body.appendChild(contactsListContainer)
}

Javascript: Async function appears to return null

I have written a function which in turn calls 2 async functions using await. It then returns a return value. When I output the return value, it shows null. Here is the code.

Function which calls 2 async functions.

async function yttExtractSFC(sfCfg, extCfg) {

    var result;
    var retVal;
    result = await yttGetSFToken(sfCfg);
    if (result.status == 'ok') {
        var accessTkn = result.data;
        result = await yttGetSFCData(sfCfg, extCfg, accessTkn);
        if (result.status == 'ok') {
            retVal = result.data;
        } else {
            retVal = result.status;
        }
    } else {
        retVal = result.status;
    }
    return retVal;
}

Here is the code I am testing with:

doRun () ;
async function doRun () {
    var x = await ysfc.yttExtractSFC(sfCfg,extCfg);
    console.log('got result:' + x.status + '/' + x.data) ;
}

No matter what the return value is, x.status and x.data are shown as ‘undefined’. So what am I doing wrong?

Textarea char count javascript in Jinja2 for-lopp

Guys I am trying to do a simple char count in textarea, and I know the code it’s working because I set it into other pages ass you can see in the picture.

As user type the progress bar shows the status at the end if hit the limit it gets red and shows the number of char from 50 until 0.

modal with progress bar working

Then, I try to apply the same code into a modal.

As you can see in the picture as well, I do have the progress bar but as I type does not work.

modal with progress bar not working

Her is my modal inside a jinja2 for loop:

<div class="modal fade" id="answer-{{post.Id}}" data-bs-backdrop="static" aria-labelledby="staticBackdropLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
    <div class="modal-content">
        <form class="needs-validation" novalidate action="{{url_for("answer_post", post_id= post.Id)}}" method="POST">
            <div class="accordion accordion-flush" id="accordion-modal">
                <div class="accordion-item">
                    <h2 class="accordion-header" id="flush-heading-post">
                        <span>{{post.Title}}</span>
                    </h2>
                </div>
            </div>

            <div class="modal-body">
                <label for="answer_post">Your Answer*</label>
                <textarea id="textarea_{{post.Id}}" type="text" name="answer_post" 
                    maxlength="500" placeholder="Elaborate your answer..." required>
                </textarea>
                
                <div class="progress mt-3">
                    <div class="progress-bar"></div>
                    <p class="remaining-chars"></p>
                </div>
                
            </div>

            <div class="modal-footer">
                <button type="submit" name="action" class="fp-blue-btn">
                    Answer
                </button>
            </div>
        </form>
    </div>
</div>

One thing I want to point out is that in my textarea for the other inputs I do not have this post.Id only textarea.

I added this Id because if I leave only id=”textarea” it apply the JS but only on the first post in the list the others nothing happens, so I tried to see if that would work but not.

The JS code is also inserted in the for loop scope so I thought would work fine but like I said it only apply for the first item in the list.

My JS that count the char and update the progress bar is as follows:

var textarea = document.getElementById("textarea_{{post.Id}}");
        var progressBar = document.getElementsByClassName("progress-bar");
        var remChars = document.getElementsByClassName("remaining-chars");

        function charCounter(inputField) {
            var maxLength = inputField.getAttribute("maxlength");
            var currentLength = inputField.value.length;
            var progressWidth = (currentLength / maxLength) * 100;
            
            progressBar.style.width = `${progressWidth}%`;
            remChars.style.display = "none";

            if (progressWidth <= 60) {
                progressBar.style.backgroundColor = "rgb(19, 160, 19)";
            } else if (progressWidth > 60 && progressWidth < 85) {
                progressBar.style.backgroundColor = "rgb(236, 157, 8)";
            } else {
                progressBar.style.backgroundColor = "rgb(241, 9, 9)";
                remChars.innerHTML = `${maxLength - currentLength} characters left`;
                remChars.style.display = "block";
            }
        }

        if (textarea != null){
            textarea.oninput = () => charCounter(textarea);
        }

So, that is what I’m having problems guys. If anyone have any idea how to make this JS apply to all textareas into my for-loop I appreciate.