-
Notifications
You must be signed in to change notification settings - Fork 90
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add linkedQueryParam implementation, tests and docs
The `linkedQueryParam` utility function creates a signal that is linked to a query parameter in the URL. This allows you to easily keep your Angular application's state in sync with the URL, making it easier to share and bookmark specific views.
- Loading branch information
Showing
19 changed files
with
1,939 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
53 changes: 53 additions & 0 deletions
53
apps/test-app/src/app/linked-query-param/linked-query-param-array.component.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
import { JsonPipe } from '@angular/common'; | ||
import { Component, effect } from '@angular/core'; | ||
import { FormsModule } from '@angular/forms'; | ||
import { linkedQueryParam } from 'ngxtension/linked-query-param'; | ||
|
||
@Component({ | ||
standalone: true, | ||
template: ` | ||
<div> | ||
@for (id of IDs; track $index) { | ||
<label> | ||
<input | ||
type="checkbox" | ||
[ngModel]="selectedCategoriesIds().includes(id)" | ||
(ngModelChange)="$event ? selectId(id) : deselectId(id)" | ||
/> | ||
{{ id }} | ||
</label> | ||
} | ||
</div> | ||
`, | ||
imports: [FormsModule, JsonPipe], | ||
styles: ` | ||
div { | ||
padding: 20px; | ||
} | ||
`, | ||
}) | ||
export default class LinkedQueryParamArrayCmp { | ||
IDs = ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10']; | ||
|
||
selectedCategoriesIds = linkedQueryParam<string[]>('selectedCategoriesIds', { | ||
parse: (x) => (x ? x.split(',').map((x) => x.trim()) : []), | ||
serialize: (x) => x.join(','), | ||
}); | ||
|
||
constructor() { | ||
effect(() => { | ||
console.log( | ||
'selectedCategoriesIds Value: ', | ||
this.selectedCategoriesIds(), | ||
); | ||
}); | ||
} | ||
|
||
selectId(id: string) { | ||
this.selectedCategoriesIds.update((x) => [...x, id]); | ||
} | ||
|
||
deselectId(id: string) { | ||
this.selectedCategoriesIds.update((x) => x.filter((x) => x !== id)); | ||
} | ||
} |
71 changes: 71 additions & 0 deletions
71
apps/test-app/src/app/linked-query-param/linked-query-param-basic.component.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
import { JsonPipe } from '@angular/common'; | ||
import { Component, effect, inject, untracked } from '@angular/core'; | ||
import { FormsModule } from '@angular/forms'; | ||
import { Title } from '@angular/platform-browser'; | ||
import { linkedQueryParam } from 'ngxtension/linked-query-param'; | ||
|
||
@Component({ | ||
standalone: true, | ||
template: ` | ||
<div> | ||
<pre>SearchQuery: {{ searchQuery() | json }}</pre> | ||
<div> | ||
<label> | ||
Different inputs same signal: | ||
<input [(ngModel)]="searchQuery" name="a" placeholder="searchQuery" /> | ||
<input [(ngModel)]="searchQuery" name="b" placeholder="searchQuery" /> | ||
</label> | ||
</div> | ||
<!-- <div>--> | ||
<!-- <label>--> | ||
<!-- Different signals same query param:--> | ||
<!-- <input--> | ||
<!-- [(ngModel)]="differentSignalWithSearchQuery"--> | ||
<!-- name="c"--> | ||
<!-- placeholder="differentSignalWithSearchQuery"--> | ||
<!-- />--> | ||
<!-- </label>--> | ||
<!-- </div>--> | ||
<button type="button" (click)="searchQuery.set('cool')">cool</button> | ||
<button type="button" (click)="searchQuery.set('great')">great</button> | ||
<button type="button" (click)="searchQuery.set(null)">Reset</button> | ||
</div> | ||
`, | ||
imports: [FormsModule, JsonPipe], | ||
styles: ` | ||
div { | ||
padding: 20px; | ||
} | ||
`, | ||
}) | ||
export default class LinkedQueryParamStringCmp { | ||
private titleService = inject(Title); | ||
|
||
searchQuery = linkedQueryParam('searchQuery', { | ||
// NOTE in docs that serialize should come after parse in order for types to work correctly | ||
// parse: (x) => x ? parseInt(x, 10) : null, | ||
// serialize: (x) => x, | ||
}); | ||
|
||
// differentSignalWithSearchQuery = linkedQueryParam('searchQuery', { | ||
// defaultValue: 'default', | ||
// }); | ||
|
||
constructor() { | ||
effect(() => { | ||
console.log('searchQuery Type: ', typeof this.searchQuery()); | ||
console.log('searchQuery Value: ', this.searchQuery()); | ||
|
||
untracked(() => { | ||
if (this.searchQuery()) { | ||
this.titleService.setTitle(this.searchQuery()!); | ||
} else { | ||
this.titleService.setTitle('No search query!'); | ||
} | ||
}); | ||
}); | ||
} | ||
} |
58 changes: 58 additions & 0 deletions
58
apps/test-app/src/app/linked-query-param/linked-query-param-booleans.component.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
import { JsonPipe } from '@angular/common'; | ||
import { Component, effect } from '@angular/core'; | ||
import { FormsModule } from '@angular/forms'; | ||
import { linkedQueryParam } from 'ngxtension/linked-query-param'; | ||
|
||
@Component({ | ||
standalone: true, | ||
template: ` | ||
<div> | ||
<pre>Show Deleted: {{ showDeleted() | json }}</pre> | ||
<div> | ||
<label> | ||
<input type="checkbox" [(ngModel)]="showDeleted" name="showDeleted" /> | ||
Show Deleted signal | ||
</label> | ||
</div> | ||
<div> | ||
<label> | ||
<input | ||
type="checkbox" | ||
[(ngModel)]="showDeletedOtherSignal" | ||
name="showDeletedOtherSignal" | ||
/> | ||
Show Deleted other signal (same query param) | ||
</label> | ||
</div> | ||
<button type="button" (click)="showDeleted.set(true)">True</button> | ||
<button type="button" (click)="showDeleted.set(false)">False</button> | ||
<button type="button" (click)="showDeleted.set(null)">Reset</button> | ||
</div> | ||
`, | ||
imports: [FormsModule, JsonPipe], | ||
styles: ` | ||
div { | ||
padding: 20px; | ||
} | ||
`, | ||
}) | ||
export default class LinkedQueryParamBooleansCmp { | ||
showDeleted = linkedQueryParam('showDeleted', { | ||
parse: (x) => x === 'true', | ||
serialize: (x) => (x ? 'true' : 'false'), | ||
}); | ||
|
||
showDeletedOtherSignal = linkedQueryParam('showDeleted', { | ||
parse: (x) => x === 'true', | ||
serialize: (x) => (x ? 'true' : 'false'), | ||
}); | ||
|
||
constructor() { | ||
effect(() => { | ||
console.log('showDeleted Type: ', typeof this.showDeleted()); | ||
console.log('showDeleted Value: ', this.showDeleted()); | ||
}); | ||
} | ||
} |
143 changes: 143 additions & 0 deletions
143
apps/test-app/src/app/linked-query-param/linked-query-param-insideObject.component.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,143 @@ | ||
import { JsonPipe } from '@angular/common'; | ||
import { Component, effect, inject } from '@angular/core'; | ||
import { FormsModule } from '@angular/forms'; | ||
import { Router } from '@angular/router'; | ||
import { linkedQueryParam } from 'ngxtension/linked-query-param'; | ||
|
||
interface MyFilters { | ||
a: string; | ||
b: string; | ||
c: { | ||
d: string; | ||
e: string; | ||
}; | ||
} | ||
|
||
@Component({ | ||
standalone: true, | ||
template: ` | ||
<div> | ||
<label> | ||
SearchQuery: | ||
<input [(ngModel)]="filterState.searchQuery" name="searchQuery" /> | ||
<button type="button" (click)="filterState.searchQuery.set(null)"> | ||
reset | ||
</button> | ||
</label> | ||
<br /> | ||
<label> | ||
Size: | ||
<select [(ngModel)]="filterState.pageSize" name="pageSize"> | ||
<option value="10">10</option> | ||
<option value="20">20</option> | ||
<option value="30">30</option> | ||
<option [ngValue]="null">null</option> | ||
</select> | ||
</label> | ||
<br /> | ||
<label> | ||
Show deleted: | ||
<input | ||
[(ngModel)]="filterState.showDeleted" | ||
name="showDeleted" | ||
type="checkbox" | ||
/> | ||
</label> | ||
<br /> | ||
<label> | ||
Page: | ||
<input [(ngModel)]="filterState.pageNumber" name="pageNumber" /> | ||
<button type="button" (click)="filterState.pageNumber.set(1)">1</button> | ||
<button type="button" (click)="filterState.pageNumber.set(2)">2</button> | ||
<button type="button" (click)="filterState.pageNumber.set(3)">3</button> | ||
<button type="button" (click)="filterState.pageNumber.set(null)"> | ||
null | ||
</button> | ||
</label> | ||
<br /> | ||
<label> | ||
SortBy: | ||
<select [(ngModel)]="filterState.sortBy" name="sortBy"> | ||
<option value="name">name</option> | ||
<option value="age">age</option> | ||
</select> | ||
</label> | ||
<br /> | ||
<label> | ||
Direction: | ||
<select [(ngModel)]="filterState.direction" name="direction"> | ||
<option value="asc">asc</option> | ||
<option value="desc">desc</option> | ||
</select> | ||
</label> | ||
<br /> | ||
<button type="button" (click)="filterState.pageSize.set(10)"> | ||
pageSize 10 | ||
</button> | ||
<button type="button" (click)="filterState.pageSize.set(20)"> | ||
pageSize 20 | ||
</button> | ||
<button type="button" (click)="filterState.pageSize.set(null)"> | ||
pageSize null | ||
</button> | ||
<button type="button" (click)="resetAll()">reset all</button> | ||
<hr /> | ||
<br /> | ||
<br /> | ||
<hr /> | ||
<br /> | ||
</div> | ||
`, | ||
imports: [FormsModule, JsonPipe], | ||
styles: ` | ||
div { | ||
padding: 20px; | ||
} | ||
`, | ||
}) | ||
export default class LinkedQueryParamInsideObjectCmp { | ||
private router = inject(Router); | ||
|
||
filterState = { | ||
searchQuery: linkedQueryParam('searchQuery'), | ||
showDeleted: linkedQueryParam('showDeleted', { | ||
parse: (x) => x === 'true', | ||
}), | ||
pageNumber: linkedQueryParam('page', { parse: (x) => (x ? +x : null) }), | ||
pageSize: linkedQueryParam('pageSize', { parse: (x) => (x ? +x : null) }), | ||
sortBy: linkedQueryParam('sortBy', { defaultValue: 'name' }), | ||
direction: linkedQueryParam<'asc' | 'desc'>('direction', { | ||
defaultValue: 'asc', | ||
}), | ||
}; | ||
|
||
constructor() { | ||
Object.keys(this.filterState).forEach((key) => { | ||
effect(() => { | ||
// @ts-ignore | ||
console.log(key, this.filterState[key]()); | ||
}); | ||
}); | ||
} | ||
|
||
resetAll() { | ||
this.router.navigate([], { | ||
queryParams: { | ||
searchQuery: null, | ||
showDeleted: null, | ||
page: null, | ||
pageSize: null, | ||
sortBy: null, | ||
direction: null, | ||
}, | ||
}); | ||
} | ||
} |
Oops, something went wrong.