Share edited collection with Observer

The challenge

Imagine creating an Angular service that needs to expose an Observable<number[]> to consumers:

numbers: Observable<number[]>;

Our requirements are:

  1. Receive the latest value upon subscription
  2. Receive the entire array every time it is modified and published

To achieve #1, internally our Observable<number[]> should be at least a BehaviorSubject<number[]>.

But how do we address requirement #2? Let's say we need to implement a method publishNumbersChange() which gets called whenever there's a change to the array:

private publishNumbersChange() {
    // Get current numbers array
    ...        

    changeArray();

    // Publish the updated numbers array
    ...
}

The question at hand

What is the preferred pattern in RxJS 5 for publishing modified arrays based on their previous state?

Since this query stems from my work with Angular, let's delve further:
How does Angular (and similar frameworks using RxJS) handle updating arrays within Observables?
Do they maintain a separate copy of the current array?

Considerations

One approach could involve storing a separate reference to the array to ensure constant access. However, this might not align with the principles of RxJS (as it introduces external state).

Alternatively, we could try the following strategy:

private publishNumbersChange() {
    // Subscribe to get the latest array value
    const subscription: Subscription = this.numbers.subscribe((numbers: number[]) => {
        // Modify the array
        changeArray();

        // Update the stream with the new array
        this.numbers.next(numbers);
    });

    // Unsubscribe to prevent memory leaks
    subscription.unsubscribe();
}

One issue with this approach (aside from its complexity and reusability) is the potential "race condition" between executing the subscriber callback and unsubscribing. This uncertainty makes it less than ideal.

Answer â„–1

If you're in need of an operator, scan might be the one you're searching for.

let subjectArray = new BehaviorSubject([]);
let array$ = subjectArray.scan((fullArr, newVal) => fullArr.concat([newVal]), [])

Scan function accumulates values over time within an observable stream, where each item in the stream receives the previously emitted value and the current value as inputs. It then applies a function to them and emits the result. For instance, the above example takes a new value and adds it to your full array, with the second parameter initializing the array as empty.

However, if you find this limitation restricting, you can get creative:

let subjectArray = new BehaviorSubject([]);
let array$ = subjectArray.scan((fullArr, {changeFunc, input}) => changeFunc(fullArr, input), []);

Now, by passing an "action" that contains a modifier function specifying how you want to alter the full array, along with any additional data required by the modifier function.

For example, you could do the following:

let changeFunc = (full, item) => full.splice(full.indexOf(item), 1);
subjectArray.next({changeFunc, input: itemToRemove});

This action removes the specified item. You can extend this approach to accommodate any array modifications.

One thing to note about scan is that subscribers only receive the accumulated value from the point they subscribed onwards. This results in:

let subjectArray = new BehaviorSubject([]);
let array$ = subjectArray.scan((fullArr, {changeFunc, input}) => changeFunc(fullArr, input), []);
let subscriber1 = array$.subscribe();
//subscriber1 gets []
let changeFunc = (full, val) => full.concat([val]);
subjectArray.next({changeFunc, input: 1});
//subscriber1 gets [1]
subjectArray.next({changeFunc, input: 2});
//subscriber1 gets [1,2]
let subscriber2 = array$.subscribe();
//subscriber2 gets [2]
subjectArray.next({changeFunc, input: 3});
//subscriber1 gets [1,2,3]
//subscriber2 gets [2,3]

In this scenario, the BehaviorSubject only stores the second event, not the entire array, while scan retains the complete array. As a result, the second subscriber only receives the second action because it wasn't subscribed during the first action. To address this issue, consider implementing a persistent subscriber pattern:

let subjectArray = BehaviorSubject([]);
let modifySubject = new Subject();
modifySubject.scan((fullArr, {changeFunc, input}) => changeFunc(fullArr, input), []).subscribe(subjectArray);

To make modifications, use next on modifySubject:

let changeFunc = (full, val) => full.concat([val]);
modifySubject.next({changeFunc, input: 1});

Your subscribers will obtain the array from the array source:

subscriber1 = subjectArray.subscribe();

In this setup, all array changes flow through the modifySubject, which broadcasts them to the BehaviorSubject for storage and distribution to subscribers. The BehaviorSubject remains persistently subscribed to the modifySubject and serves as the sole subscriber to ensure the entire history of actions is preserved.

Below are a few sample usages with the mentioned setup:

// insert 1 at the end
let changeFunc = (full, value) => full.concat([value]);
modifySubject.next({changeFunc, input: 1});

// insert 1 at the start
let changeFunc = (full, value) => [value].concat(full);
modifySubject.next({changeFunc, input: 1});

// remove 1
let changeFunc = (full, value) => full.splice(full.indexOf(value), 1);
modifySubject.next({changeFunc, input: 1});

// change all instances of 1 to 2
let changeFunc = (full, value) => full.map(v => (v === value.target) ? value.newValue : v);
modifySubject.next({changeFunc, input: {target: 1, newValue: 2}});

You can encapsulate these functions in a "publishNumbersChange" method. The implementation varies based on your requirements—options include creating functions like:

insertNumber(numberToInsert:number) => {
   let changeFunc = (full, val) => full.concat([val]);
   publishNumbersChange(changeFunc, numberToInsert);
}

publishNumbersChange(changeFunc, input) => {
   modifySubject.next({changeFunc, input});
}

Alternatively, you can define an interface, create classes, and leverage those:

publishNumbersChange({changeFunc, input}) => {
   modifySubject.next({changeFunc, input});
}

interface NumberArrayModifier {
    changeFunc: (full: number[], payload:any) => number[];
    input: any;
}

class InsertNumber implements NumberArrayModifier {
    changeFunc = (full: number[], payload: number): number[] => full.concat([payload]);
    input: number;
    constructor(numberToInsert:number) {
        this.input = numberToInsert;
    }
}

publishNumbersChange(new InsertNumber(1));

This approach can be extended to any array modification. A final tip: lodash proves invaluable when defining modifiers in such systems.

Considering an Angular service context, the following represents a simple yet somewhat non-reusable implementation:

const INIT_STATE = [];
@Injectable()
export class NumberArrayService {
    private numberArraySource = new BehaviorSubject(INIT_STATE);
    private numberArrayModifierSource = new Subject();
    numberArray$ = this.numberArraySource.asObservable();

    constructor() {
        this.numberArrayModifierSource.scan((fullArray, {changeFunc, input?}) => changeFunc(fullArray, input), INIT_STATE).subscribe(this.numberArraySource);
    }

    private publishNumberChange(changeFunc, input?) {
        this.numberArrayModifierSource.next({changeFunc, input});
    }

    insertNumber(numberToInsert) {
        let changeFunc = (full, val) => full.concat([val]);
        this.publishNumberChange(changeFunc, numberToInsert);
    }

    removeNumber(numberToRemove) {
        let changeFunc = (full, val) => full.splice(full.indexOf(val),1);
        this.publishNumberChange(changeFunc, numberToRemove);
    }

    sort() {
        let changeFunc = (full, val) => full.sort();
        this.publishNumberChange(changeFunc);
    }

    reset() {
        let changeFunc = (full, val) => INIT_STATE;
        this.publishNumberChange(changeFunc);
    }
}

Usage involves subscribing to numberArray$ and modifying the array by invoking functions. This straightforward pattern allows for easy extension to tailor functionality according to requirements. It safeguards access to your number array by ensuring modifications align with the API, state, and maintaining consistency between subjects.

How can this be made more generic/reusable?

export interface Modifier<T> {
    changeFunc: (state: T, payload:any) => T;
    payload?: any;
}

export class StoreSubject<T> {
    private storeSource: BehaviorSubject<T>;
    private modifierSource: Subject<Modifier<T>>;
    store$: Observable<T>;

    publish(modifier: Modifier<T>): void {
        this.modifierSource.next(modifier);
    }

    constructor(init_state:T) {
        this.storeSource = new BehaviorSubject<T>(init_state);
        this.modifierSource = new Subject<Modifier<T>>();
        this.modifierSource.scan((acc:T, modifier:Modifier<T>) => modifier.changeFunc(acc, modifier.payload), init_state).subscribe(this.storeSource);
        this.store$ = this.storeSource.asObservable();
    }
}

The modified service would appear as follows:

const INIT_STATE = [];
@Injectable()
export class NumberArrayService {
    private numberArraySource = new StoreSubject<number[]>(INIT_STATE);
    numberArray$ = this.numberArraySource.store$;

    constructor() {
    }

    insertNumber(numberToInsert: number) {
        let changeFunc = (full, val) => full.concat([val]);
        this.numberArraySource.publish({changeFunc, payload: numberToInsert});
    }

    removeNumber(numberToRemove: number) {
        let changeFunc = (full, val) => full.splice(full.indexOf(val),1);
        this.numberArraySource.publish({changeFunc, payload: numberToRemove});
    }

    sort() {
        let changeFunc = (full, val) => full.sort();
        this.numberArraySource.publish({changeFunc});
    }

    reset() {
        let changeFunc = (full, val) => INIT_STATE;
        this.numberArraySource.publish({changeFunc});
    }
}

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

Refilling state through an NgRx Effect

I am facing a situation where I have JSON data stored in my effect, which was initially generated using JSON.stringify(state), and now I need to insert that JSON string back into the state in order to update the application. As someone new to Angular and N ...

Instead of receiving my custom JSON error message, Express is showing the server's default HTML error page when returning errors

I have set up a REST api on an Express server, with a React app for the front-end. The design includes sending JSON to the front-end in case of errors, which can be used to display error messages such as modals on the client side. Below is an example from ...

Tips for centering a div horizontally when it exceeds the width of a mobile screen

My challenge is to create a circular div element that is wider than the mobile screen and perfectly centered. This circular div needs to be an exact circle, specifically targeted for mobile screens. I've attempted to achieve this using the code in th ...

How to retrieve query parameters using Angular 2's HTTP GET method

Seeking assistance on my Ionic 2 app built with Angular 2 and TypeScript. I am familiar with older versions of Angular, but still adjusting to this new environment. I have set up my API with basic query strings (e.g domain.com?state=ca&city=somename) ...

When using jquery.hide() and show(), the content within the div disappears momentarily before reappearing

Hello! I am experiencing an issue where the content of my div disappears after using a jQuery hide() and show() function. It seems to be gone. <li class="gg3"><a class="link" href="#" data-rel="content3">Link1</a></li> <div clas ...

Using Selenium to interact with drop-down lists using div tags instead of select tags

As a newcomer to automated testing using Selenium Web Driver, I am struggling to test drop down lists for the location type without relying on the select command. The element in question is enclosed within a div tag. I attempted sending keys but that meth ...

Submit information by utilizing' content-type': 'application/x-www-form-urlencoded' and 'key': 'key'

Attempting to send data to the server with a content-type of 'application/xwww-form-urlencode' is resulting in a failure due to the content type being changed to application/json. var headers= { 'content-type': 'applica ...

When trying to generate a popOver in Ionic, an error message "<TypeError: ev.target.getBoundingClientRect is not a function>" may be displayed

I'm currently working on implementing a popover that appears when a mouse click event is triggered. However, I've encountered an issue where the Create() method of the popover gets called upon event activation, but I keep receiving the following ...

Angular allows cross-origin requests with Access-Control-Allow-Origin

Hi, I am facing an issue with the update function in my app. Whenever I try to update, it shows me the following error message: Failed to load http:// localhost:8080/../update: Response to preflight request doesn't pass access control check: No &apo ...

Exploring the Integration of JSONAPI Included with Angular Observables

Currently, I am in the process of learning how to extract Drupal 8 content into my Angular 7 application using jsonapi. This journey started after reading Preston So's enlightening book on Decoupled Drupal in Practice. However, the guide fell short of ...

Resetting a Material UI search filter TextField back to its initial state after clearing it

Unleashing the power of ReactJS alongside Material UI has been a journey of ups and downs for me. While I managed to create a versatile search filter component for my data tables that worked like a charm, I now find myself at a crossroads. My new goal is t ...

Upgrade to Jquery 2.x for improved performance and utilize the latest ajax code enhancements from previous versions

We are encountering a minor issue with Jquery while loading numerous ajax files in our system. Currently, we are using Jquery 2.x and need to be able to operate offline on IE 9+. Previously, when working with Jquery 1.x, we were able to load ajax files fro ...

Using JQuery, you can toggle a newly created DIV element by linking it to `$(this)` instead of `$(this).closest()`

In the comment section, there is a link called "Reply" that triggers a pop-up comment box when clicked. However, I want the comment box to disappear if the "reply" button is clicked again, as it currently keeps opening more comment boxes. $('.replyli ...

Tips for eliminating the trailing slash from the end of a website article's URL

I've recently delved into learning Gatsby, and I've encountered an issue with the Open Graph tag in my project. The og:image is displaying a different image than the intended thumbnail for the article. Here's an example article - . When try ...

Identify the locality and apply it to the root module of Angular 2 by utilizing a provider

I am working on a function that detects the current locale and I need to set it in the root App module of Angular 2 using a provider so that I can access it in all other components. I understand that I can achieve this by following the code below: { pr ...

"Obtaining a URL that begins with using jQuery: A Step-by-

How can I extract the full URL that starts with http://sin1.g.adnxs.com Here is the code snippet: <div id="testingurl"> <div class="ads98E6LYS20621080"> <!-- This script is dynamically generated --> <iframe src="http://testing ...

Angular ui router - Transitioning from one state to the same state, when no parameters are provided, results in

Check out the Plunker Demo <script> var myapp = angular.module('myapp', ["ui.router"]) myapp.config(function($stateProvider, $urlRouterProvider) { // If there is no matching URL, go to /dashboard $urlRouterProvider. ...

Tips for managing nested data in Angular 4 using a Bootstrap 4 data-table

I am currently using the Data Table from a GitHub project found at: https://github.com/afermon/angular-4-data-table-bootstrap-4-demo. It works perfectly with data structured in a key-value format like the sample provided. However, I am facing challenges wh ...

Dynamic Divider for Side-by-Side Menu - with a unique spin

I recently came across a question about creating responsive separators for horizontal lists on Stack Overflow While attempting to implement this, I encountered some challenges. document.onkeydown = function(event) { var actionBox = document.getElementB ...

Is it possible to rewrite this function recursively for a more polished outcome?

The function match assigns a true or false value to an attribute (collapsed) based on the value of a string: function match(children) { var data = $scope.treeData for (var i = 0; i < data.length; i++) { var s = data[i] for (var ...