Is there a way to receive a callback specifically for changes to a subset of nested properties within this system?

Imagine having a "type" structured like this:

{
  a: {
    b: {
      c: {
        d: string
        e: boolean
      }
    },
    x: string
    y: number
    z: string
  }
}

What if you wanted to be notified at each object node when all the children are resolved to a value? For example:

const a = new TreeObject()
a.watch((a) => console.log('a resolved', a))

const b = a.createObject('b')
b.watch((b) => console.log('b resolved', b))

const c = b.createObject('c')
c.watch((c) => console.log('c resolved', c))

const d = c.createLiteral('d')
d.watch((d) => console.log('d resolved', d))

const e = c.createLiteral('e')
e.watch((e) => console.log('e resolved', e))

const x = a.createLiteral('x')
x.watch((x) => console.log('x resolved', x))

const y = a.createLiteral('y')
y.watch((y) => console.log('y resolved', y))

d.set('foo')
// logs:
// d resolved

e.set('bar')
// logs:
// e resolved
// c resolved
// b resolved

y.set('hello')
// logs:
// y resolved

x.set('world')
// logs:
// x resolved
// a resolved

This is the base case. The more complex scenario, which I've been trying to tackle, involves matching against a subset of properties like this:

// receive 'b' only if b.c.d is resolved.
// '3' for 3 args
a.watch3('b', {
  c: {
    d: true
  }
}, () => {
  console.log('b with b.c.d resolved')
})

You can have multiple "watchers" per property node as well:

a.watch3('b', { c: { d: true } }, () => {
  console.log('get b with b.c.d resolved')
})

a.watch3('b', { c: { e: true } }, () => {
  console.log('get b with b.c.e resolved')
})

a.watch2('x', () => {
  console.log('get x when resolved')
})

// If we were to start from scratch setting properties fresh:
x.set('foo')
// logs:
// get x when resolved

e.set('bar')
// logs:
// get b with b.c.e resolved

How can this be neatly set up? I have been struggling to figure it out but haven't made much progress. You can check my attempts in this TS playground.

type Matcher = {
  [key: string]: true | Matcher
}

type Callback = () => void

class TreeObject {
  properties: Record<string, unknown>

  callbacks: Record<string, Array<{ matcher?: Matcher, callback: Callback }>>

  parent?: TreeObject

  resolved: Array<Callback>

  constructor(parent?: TreeObject) {
    this.properties = {}
    this.callbacks = {}
    this.parent = parent
    this.resolved = []
  }

  createObject(name: string) {
    const tree = new TreeObject(this)
    this.properties[name] = tree
    return tree
  }
  
  createLiteral(name: string) {
    const tree = new TreeLiteral(this, () => {
      // Logic to track and trigger the callback once fully matched
    })
    this.properties[name] = tree
    return tree
  }

  watch3(name: string, matcher: Matcher, callback: Callback) {
    const list = this.callbacks[name] ??= []
    list.push({ matcher, callback })
  }

  watch2(name: string, callback: Callback) {
    const list = this.callbacks[name] ??= []
    list.push({ callback })
  }

  watch(callback: Callback) {
    this.resolved.push(callback)
  }
}

class TreeLiteral {
  value: any

  parent: TreeObject

  callback: () => void

  resolved: Array<Callback>

  constructor(parent: TreeObject, callback: () => void) {
    this.value = undefined
    this.parent = parent
    this.callback = callback
    this.resolved = []
  }

  set(value: any) {
    this.value = value
    this.resolved.forEach(resolve => resolve())
    this.callback()
  }

  watch(callback: Callback) {
    this.resolved.push(callback)
  }
}

const a = new TreeObject()
a.watch(() => console.log('a resolved'))

const b = a.createObject('b')
b.watch(() => console.log('b resolved'))

const c = b.createObject('c')
c.watch(() => console.log('c resolved'))

const d = c.createLiteral('d')
d.watch(() => console.log('d resolved'))

const e = c.createLiteral('e')
e.watch(() => console.log('e resolved'))

const x = a.createLiteral('x')
x.watch(() => console.log('x resolved'))

const y = a.createLiteral('y')
y.watch(() => console.log('y resolved'))

d.set('foo')
// logs:
// d resolved

e.set('bar')
// logs:
// e resolved
// c resolved
// b resolved

y.set('hello')
// logs:
// y resolved

x.set('world')
// logs:
// x resolved
// a resolved

How can the methods like watch3 be defined to handle matchers and call the callback appropriately when the specified properties are fulfilled?

The challenge lies in handling values that may have already been resolved before adding watchers, as well as future resolutions after adding them. The "matcher" syntax resembles GraphQL queries, where you define an object tree with leaves set to true on desired properties.

Answer №1

Here are some initial musings:

  • As I see it, the first argument of

    a.watch3('b', {c: {d: true}}, cb)
    must match one of the properties, and the Matcher object should correspond to the value of that property. To simplify things, I propose mapping the Matcher object with the current object (this) and nesting b inside that Matcher, eliminating the need for the first argument:

    a.watch3({b: {c: {d: true}}}, cb);
    
  • I advocate for using a single watch method across all signatures, where the callback is always the first argument and the Matcher object is an optional second argument. In my view, the name argument is superfluous (referencing previous point).

  • In my assumption, a callback can only be triggered once. This becomes pertinent in scenarios like the following:

    const a = new TreeObject();
    const b = a.createObject('b');
    const c = b.createObject('c');
    const d = c.createLiteral('d');
    
    a.watch(() => console.log("a resolved"));
    
    d.set('foo'); // This results in "a resolved"
    
    // This reverts 'a' back to unresolved
    const e = c.createLiteral('e');
    // Subsequent resolution of 'a' will not trigger the same callback
    e.set('bar'); // No longer triggers "a resolved"
    // Let's revert 'a' again...
    const f = c.createLiteral('f');
    // But this time we add a new callback before the resolution happens:
    a.watch(() => console.log("a resolved AGAIN"));
    f.set('baz'); // Now triggers "a resolved AGAIN" but not earlier
    

    This assumption implies that a callback ought to be unregistered after being called.

  • If a callback is registered without any literals present, the object remains considered as not yet resolved -- resolution requires at least one literal within the object structure (downstream), with all downstream literals having received values (or subsets, when using a Matcher object).

  • If a provided Matcher object references a partially or entirely absent structure, the registered callback will not execute until the structure is fully built out and all associated literals have been assigned values. Thus, we may require a concept of 'pending matchers' that necessitates checking whenever a missing property is created that permits one or more matchers to apply to said property.

  • If a true value exists within the Matcher object where the actual object structure features a deeper nested object rather than a literal, that true value indicates that "all below this point" must have obtained values.

  • If the Matcher object contains an object while the actual object comprises a literal, that matcher remains unresolved indefinitely.

  • In enhancing this approach, matchers are transformed into standard watchers on endpoint nodes (without a matcher) as soon as feasible (once the corresponding structure is complete). This allows management through counters updated upstream from the literal to the root. For instance, when a counter reaches zero, it signifies full resolution of all required items. It is crucial to note that each matcher object will generate its own callbacks for individual endpoints, tracking a separate counter which, upon reaching zero, triggers the original callback.

The code implementation could look something like this:

<!-- The TypeScript code mentioned in the original text would be written here -->

Explore this further with test functions on TS Playground

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Troubleshooting route structures and layouts in Next.js

I'm working on a web app and I'm looking to add a feature similar to this. Currently, I have Navbar (Header) and footer components in place. Can anyone suggest how I should structure my files? I've considered using route grouping, but it do ...

What is the correct way to implement Vue.use() with TypeScript?

I am trying to incorporate the Vuetify plugin into my TypeScript project. The documentation (available at this link) suggests using Vue.use(), but in TypeScript, I encounter the following error: "error TS2345: Argument of type '{}' is not assign ...

Granting TypeScript classes the ability to utilize state from React hooks

My current project involves creating a game using React for the user interface and Typescript classes to manage the game state. Below are some examples of the classes I have implemented to handle my data: export class Place extends Entity { // Class pro ...

Is it necessary for TypeScript classes that are intended for use by other classes to be explicitly exported and imported?

Is it necessary to explicitly export and import all classes intended for use by other classes? After upgrading my project from Angular 8 to Angular 10, I encountered errors that were not present before. These issues may be attributed to poor design or a m ...

What is the proper way to access the global `angular` variable in Angular and TypeScript when using AngularJS?

I'm currently integrating an Angular 4 component into a large monolithic AngularJS application. The challenge I face lies in the restrictions of the AngularJS project's build system, which limits me to only modifying the project's index.html ...

Implementing a Single handleChange Event for Multiple TextFields

How can I use the same handleChange event for multiple TextFields in React? import * as React from "react"; import { TextField} from 'office-ui-fabric-react/lib/TextField'; export interface StackOverflowState { value: string; } export de ...

The absence of a template or render function in a Vue.js 3 and Quasar 2 component has resulted in an

I am currently working on creating a dynamic component and passing a prop to it. However, I am encountering a warning message that says: Component is missing template or render function. Although the component is being rendered, I am still receiving the wa ...

An unexpected error occurs when attempting to invoke the arrow function of a child class within an abstract parent class in Typescript

Here is a snippet of code that I'm working on. In my child class, I need to use an arrow function called hello(). When I try calling the.greeting() in the parent class constructor, I encounter an error: index.ts:29 Uncaught TypeError: this.hello is ...

Angular 9 ensures that the component template is validated and loaded before the constructor logic is executed

Recently switched from Angular 8 to Angular 9 (without IVY) and encountered some unusual errors indicating that services injected in components are undefined in getters. Upon investigation, I discovered that the getter is being called before the constructo ...

Tips for establishing communication between a server-side and client-side component in Next.js

I am facing a challenge in creating an interactive component for language switching on my website and storing the selected language in cookies. The issue arises from the fact that Next.js does not support reactive hooks for server-side components, which ar ...

Automatically select a value in MUI AutoComplete and retrieve the corresponding object

I recently set up a list using the MUI (v4) Select component. I've received a feature request to make this list searchable due to its extensive length. Unfortunately, it appears that the only option within MUI library for this functionality is the Au ...

Managing null values in RxJS map function

I'm facing a scenario where my Angular service retrieves values from an HTTP GET request and maps them to an Observable object of a specific type. Sometimes, one of the properties has a string value, while other times it's null, which I want to d ...

Angular - Set value only if property is present

Check if the 'rowData' property exists and assign a value. Can we approach it like this? if(this.tableObj.hasOwnProperty('rowData')) { this.tableObj.rowData = this.defVal.rowData; } I encountered an error when attempting this, specif ...

Check if a string contains only special characters and no letters within them using regular expressions

My goal is to validate a string to ensure it contains letters only between two '#' symbols. For example: #one# + #two# - is considered a valid string #one# two - is not valid #one# + half + #two# - is also not a valid string (only #one# and # ...

The concept of ExpectedConditions appears to be non-existent within the context of

Just starting out with protractor and currently using version 4.0.2 However, I encountered an error with the protractor keyword when implementing the following code: import { browser } from 'protractor/globals'; let EC = protractor.Expe ...

Verify the data type received from the event emitter

I want to develop a strict event emitter for TypeScript, but I'm not sure if it can be done. Let's say I create a listener for my emitter: // define listener @listen('my-custom-event') function userListener(data: IUser){ // handle d ...

Ways to personalize the interface following the selection of an item from a dropdown menu

Currently, I am using Vue.js and Typescript for my project work. I have a requirement to customize the interface by making adjustments based on the selected item from a drop-down list. <b-form-select id="input-topic" ...

Unraveling deeply nested object structures within an array of objects

In my Typescript project, I am working with an object structure that looks like this: { a: "somedata", b: "somedata2", c: [ { name: "item1", property1: "foo", property2: "bar" ...

Creating nested Angular form groups is essential for organizing form fields in a hierarchical structure that reflects

Imagine having the following structure for a formGroup: userGroup = { name, surname, address: { firstLine, secondLine } } This leads to creating HTML code similar to this: <form [formGroup]="userGroup"> <input formCon ...

What is the best way to showcase a view on the same page after clicking on a link/button in Angular?

Is there a way to show a view on the same page in an Angular application when a link is clicked? Rather than opening a new page, I want it displayed alongside the list component. How can this be accomplished? Here's an illustration of my goal: I&apos ...