@Component({
selector: 'app-filter',
template: `
<input type="checkbox"
[checked]="isActive"
(change)="toggle()" />
`,
...
})
export class FilterComponent {
isActive: boolean;
toggle(): void {
this.isActive = !this.isActive;
}
}
Local UI state
@Component({
selector: 'app-filter',
template: `
<input type="checkbox"
[checked]="isActive"
(change)="toggle()" />
`,
...
})
export class FilterComponent {
@Input()
isActive: boolean;
@Output()
activate = new EventEmitter<void>();
@Output()
deactivate = new EventEmitter<void>();
toggle(): void {
if(this.isActive) {
this.deactivate.emit();
} else {
this.activate.emit();
}
}
}
Decoupled from state
@Component({
selector: 'app-filters',
template: `
<app-filter *ngFor="let filter of filters"
[isActive]="isActive(filter)"
(activate)="activate(filter)"
(deactivate)="deactivate(filter)"
`,
...
})
export class FiltersComponent<T extends { id: string }> {
filters: T[];
private activateFilters: string[] = [];
activate(filter: T) {
this.activateFilters.push(filter.id);
}
deactivate(filter: T) {
const index = this.activateFilters.indexOf(filter.id);
if(index !== -1) {
this.activateFilters.splice(index, 1);
}
}
isActive(filter: T) {
return this.activateFilters.some(id => id === filter.id);
}
}
Local UI state
local state
Array lookup each event
Array lookup each render
const filters: T[] = [
filter1,
filter2,
filter3,
...
filter10,
];
const activeFilterIds: string[] = [
'filter8',
'filter9',
'filter10'
];
function isActive(filter: T) {
return activeFilters.some(id => id === filter.id);
}
const activeFilters = filters.filter(isActive);
// 1 * 1 + 1 * 2 + 1 * 3 + 7 * 3 = 28 checks
Problem
const filters: T[] = [
filter1,
filter2,
filter3,
...
filter10,
];
const activeFilterIds: HashMap<boolean> = {
'filter8': true,
'filter9': true,
'filter10': true
};
function isActive(filter: T) {
return activeFilters[filter.id] === true;
}
const activeFilters = filters.filter(isActive);
// 10 * 1 = 10 === checklist.length checks
HashMap
interface HashMap<T> {
[key: string]: T;
}
@Component({
selector: 'app-filters',
template: `
<app-filter *ngFor="let filter of filters"
[isActive]="isActive(filter)"
(activate)="activate(filter)"
(deactivate)="deactivate(filter)"
`,
...
})
export class FiltersComponent<T extends { id: string }> {
filters: T[] = [];
private activateFilterIds: HashMap<boolean> = {};
activate(filter: T) {
this.activateFilterIds[filter.id] = true;
}
deactivate(filter: T) {
this.activateFilterIds[filter.id] = false;
}
isActive(filter: T) {
return this.activateFilterIds[filter.id] === true;
}
}
Local UI state
@Component({
selector: 'app-filters',
template: `
<app-filter *ngFor="let filter of filters"
[isActive]="isActive(filter)"
(activate)="activate(filter)"
(deactivate)="deactivate(filter)"
`,
...
})
export class FiltersComponent<T extends { id: string }> {
filters: T[] = [];
private activateFilters: string[] = [];
activate(filter: T) {
this.activateFilters.push(filter.id);
}
deactivate(filter: T) {
const index = this.activateFilters.indexOf(filter.id);
if(index !== -1) {
this.activateFilters.splice(index, 1);
}
}
isActive(filter: T) {
return this.activateFilters.some(id => id === filter.id);
}
}
const normalizedData: NormalizedCollection<T> = {
values: {
'filter1': filter1,
'filter2': filter2,
// ...
'filter10': filter10
},
ids: [
'filter1',
'filter2',
// ...
'filter10'
];
};
const collection = toArray(normalizedData);
const normalized = normalize(collection);
Sorted normalized collections
interface Normalized<T> {
values: HashMap<T>;
ids: string[];
}
function normalize<T>(collection: T[]): Normalized<T> {
return collection.reduce(({values, ids}, item) => ({
values: {
...values,
[item.id]: item
},
ids: ids.concat(item.id)
}), { values: {}, ids: []});
}
function toArray<T>({ values, ids }: Normalized<T>): T[] {
return ids.map(id => values[id]);
}
// Get by id
const result = data.values[id];
// Get by ids (unordered)
const byIds = ids.map(id => data.values[id]);
// Get by ids (ordered)
const byIdsOrdered = ids.map(id => data.values[id]);
// Get by id
const i = data.indexOf(id);
const byId = data[i];
// Get by ids (unordered)
const byIds = data.filter(id =>
ids.indexOf(id) !== -1
);
// Get by ids (ordered)
const byIdsOrdered = ids.map(id => {
const i = data.indexOf(id);
return data[i];
});
Server
T[]
Normalize data
Normalized<T>
...
interface FiltersResponse<T> {
filters: T[];
}
interface FiltersData<T> {
filters: Normalized<T>;
}
@Injectable()
export class FiltersService<T> {
constructor(private http: HttpClient) {}
get(): Observable<FiltersData<T>> {
return this.http.get('...')
.pipe(
map(({ filters }: FiltersResponse<T>) => ({
filters: normalize(filters)
}))
);
}
}
Normalized state
Server
T[]
Normalize data
Normalized<T>
interface Filter {
id: string;
// ...
}
interface FiltersData {
filters: Normalized<Filter>;
activeFilters: HashMap<boolean>;
}
interface EnrichedFilter extends Filter {
active: boolean;
}
interface FiltersState {
filters: EnrichedFilter[];
}
function selectState(data: FiltersData) {
const { filters, activeFilters } = data;
const { values, ids } = filters;
return {
filters: ids.map(id => ({
...values[id],
active: activeFilters[id]
})) as EnrichedFilter[]
}
}
@Component({
selector: 'app-filters',
template: `
<ng-container *ngIf="state$ | async as state">
<app-filter *ngFor="let filter of state.filters"
[isActive]="filter.active"
(activate)="activate(filter)"
(deactivate)="deactivate(filter)"
</ng-container>
`,
// ...
})
export class FiltersComponent implements OnInit {
state$: Observable<FiltersState>;
constructor(private service: FiltersService) {}
ngOnInit() {
this.state$ = this.service.get()
.pipe(
map(selectFiltersState)
);
}
// ...
}
@Component({
selector: 'app-filters',
template: `
<ng-container *ngIf="data$ | async as data">
<app-filter *ngFor="let filter of data.filters"
[isActive]="isActive(filter, data.activeFilters)"
(activate)="activate(filter)"
(deactivate)="deactivate(filter)"
</ng-container>
`,
// ...
})
export class FiltersComponent implements OnInit {
data$: Observable<FiltersData>;
constructor(private service: FiltersService) {}
ngOnInit() {
this.data$ = this.service.get();
}
isActive(filter: Filter, activeFilters: HashMap<boolean>) {
return activeFilters[filter.id] === true;
}
// ...
}
// interface EnrichedFilter extends Filter {
// active: boolean;
// }
interface FiltersState {
// filters: EnrichedFilter[];
activeFilters: EnrichedFilter[];
}
function selectState(data: FiltersData) {
// const { filters, activeFilters } = data;
// const { values, ids } = filters;
// return {
// filters: ids.map(id => ({
// ...values[id],
// active: activeFilters[id]
// })) as EnrichedFilter[],
activeFilters: Object.keys(activeFilters)
.map(id => values[id])
// }
}
@Component({
selector: 'app-filters',
template: `
<ng-container *ngIf="state$ | async as state">
...
<div *ngFor="let filter of state.activeFilters">
Active filter: {{ filter.id }}
</div>
</ng-container>
`,
// ...
})
export class FiltersComponent implements OnInit {
// state$: Observable<FiltersState>;
// constructor(private service: FiltersService) {}
// ngOnInit() {
// this.state$ = this.service.get()
// .pipe(
// map(selectFiltersState)
// );
// }
// ...
}
Server
T[]
Normalize data
Normalized<T>
const updates = {};
const validatedState = {
activeFilters: {
'filter1': true
},
// ...
}
// Merge validated state + updates
const state = {
isOptimistic: false,
activeFilters: {
'filter1': true
},
// ...
}
Update
const updates = {
activeFilters: {
'filter2': true
},
};
const validatedState = {
activeFilters: {
'filter1': true
},
// ...
}
// Merge validated state + updates
const state = {
isOptimistic: true,
activeFilters: {
'filter1': true,
'filter2': true
},
// ...
}
Update
const optimisticState = {
isOptimistic: true,
activeFilters: {
'filter1': true,
'filter2': true // update
},
// ...
}
// Successful call
validatedState = optimisticState;
updates = {};
// Merge validated state + updates
const state = {
isOptimistic: false,
activeFilters: {
'filter1': true,
'filter2': true
},
// ...
}
Update
const optimisticState = {
isOptimistic: true,
activeFilters: {
'filter1': true,
'filter2': true // update
},
// ...
}
// Failed call
updates = {};
// Merge validated state + updates
const state = {
isOptimistic: false,
activeFilters: {
'filter1': true
},
// ...
}