Skip to content

Instantly share code, notes, and snippets.

@jsonberry
Last active August 31, 2023 08:45
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jsonberry/496a024e63b86b4581807bbabd038986 to your computer and use it in GitHub Desktop.
Save jsonberry/496a024e63b86b4581807bbabd038986 to your computer and use it in GitHub Desktop.
Angular: RxJS + Lodash for getting deeply nested data out of an Observable stream and into presentational components
// .js extension for syntax highlighting - all files are actually .ts
// A general service that makes an HTTP call to an API
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http'
@Injectable()
export class ApiService {
constructor (
private http: HttpClient,
) { }
get data$ () {
return this.http.get('/some/api') // Returning an Observable
}
}
// Presentational List Component
import { Component, OnInit, Input } from '@angular/core';
@Component({
selector: 'my-list-item',
template: `
<p>{{ message }}</p> // "an orginal message string hamburgers"
<p>{{ date | date }}</p> // "Oct 4, 2017"
`,
styleUrls: ['./something-cool-data.component.scss']
})
export class MyListItemComponent implements OnInit {
public date // This just helpes to make the template a little bit cleaner
public message
@Input() set props(props) { // Just showing off a way to do some stuff in an @Input if you need, like data checking
const { date, message }: { date: Date, message: string } = props
Object.assign(this, {
date,
message
})
}
}
// Presentational List Component
import { Component, OnInit, Input } from '@angular/core';
@Component({
selector: 'my-presentational-list',
template: `
<my-list-item *ngFor="let prop of props"
[props]="item">
</my-list-item>
`,
styleUrls: ['./something-cool-data.component.scss']
})
export class MyPresentationalListComponent implements OnInit {
@Input() props
}
import { filter, flow, getOr, map, pick } from 'lodash/fp'
export {
mySelector
}
// Assume we have a data structure like this:
interface MyCoolInterface {
whateverIDontNeedIt: object[]
id: string
somethingElseIDonotCareAbout: whocares[]
events: Event[] // We care about these
}
interface Event {
type: 'tweet' | 'email' | 'snailMail' | 'chat'
data: {
blob: { // just to represent some deeply nested data
id: string
message: string
dateTime: Date
read: boolean
}
}
}
/*
This is the data structure that will be fed into the presentational component
example: https://runkit.com/jsonberry/59d5bdf05acdf30012d339c2
[{
dateTime: "2017-10-04T05:54:45.743Z"
message: "an orginal message string hamburgers"
}]
*/
function mySelector (data$): Observable {
return data$.map(() => { // This is still returning an Observable, but we have manipulated the output for presentation
return transformData(data$) // The async pipe in the Data component is handling the Observable subscription and teardown
}) // ---> IMPORTANT NOTE ---> this example is not fully fleshed out, when I have some more time I'll update it, but this is "basically" how it works
}
/*
The secret sauce for data transforming and getting deeply nested values
Some reading on the techniques:
https://medium.com/making-internets/why-using-chain-is-a-mistake-9bc1f80d51ba
https://blog.pragmatists.com/higher-order-functions-in-lodash-3283b7625175
*/
const unreadChatMessages = obj => obj.type === 'chat' && !obj.data.blob.read
const formatDate = obj => ({ ...obj, dateTime: obj.dateTime.toISOString() })
const addHamburgers = obj => ({ ...obj, message: `${obj.message} hamburgers` })
const transformData = flow(
getOr([], 'events'),
filter(unreadChatMessages),
map(
flow(
getOr({}, 'data.blob'),
pick(['message', 'dateTime']),
formatDate,
addHamburgers
)
)
)
// A data component, repsonsible for connecting to Angular services, and providing data to presentational components
import { Component, OnInit } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { ApiService } from '../api.service'; // Our majesteic API service
import { mySelector } from './myselector'; // A magical place full of wonder and joy
@Component({
selector: 'my-cool-data',
template: `
<my-presentional-list
[props]="data$ | async"> // Using the async pipe helps with all the Observable cleanup mumbojumbo
</my-presentional-list>
`,
styleUrls: ['./something-cool-data.component.scss']
})
export class MyCoolDataComponent implements OnInit {
public data$: Observable<any>
constructor (
private apiService: ApiService
) { }
ngOnInit () {
this.data$ = this.apiService.data$
.let(mySelector) // https://www.learnrxjs.io/operators/utility/let.html
} // We'll use the `let` Observable method to pass the Observable stream through a transform
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment