#ngrx 4 how to filter current loaded data - ngrx

I am working on a new angular 4 plus #ngrx 4 project.
I wish to have a searching function on the loaded data.
For example, all the contacts info have been loaded in the component.
The contacts list will be filtered which contact name matched with the search text.
Please see screenshot
As the data is existed in store and I do not wish to call web api service again.
Any idea or demo code would be appreciated.

You can follow this flow to search what you need on already fetched content:
Use something like '(input)'='searchInputChange$.next(search)' in your input. So, each time the user changes the input, it will trigger our research.
Then, on your component, on the constructor, each time searchInputChange$ changes, we trigger a new SearchAction. Then, we will change our filtered contents on the reducers and the result will be inserted into contents$. On ngOnInit we just load the data from api the first time.
I'm using a model called Content, just an example, that has a string parameter title. We will use this field to filter our contents based on the search input.
import { Component, OnInit } from '#angular/core';
import { Store } from '#ngrx/store';
import { Subject } from 'rxjs/Subject';
import {of} from 'rxjs/observable/of';
/** ngrx **/
import {AppState} from '../../app-state.interface';
import * as searchActions from './actions/search.actions';
/** App Models **/
import { Content } from './models/content.model';
export class SearchComponent implements OnInit {
searchInputChange$ = new Subject<string>();
contents$: Observable<Array<Content>>;
constructor(private _store: Store<AppState>) {
this.searchInputChange$
.switchMap((text: string) => of(text))
.subscribe((text: string) => this._store.dispatch(new searchActions.SearchAction(text)));
this.contents$ = this._store.select(getSearchedContents);
}
ngOnInit() {
this._store.dispatch(new searchActions.LoadAction());
}
}
Then, we'll have our SearchActions. Load is triggered on the init of our component, fetches some contents from api. LoadSuccess is emitted on the effect of the load action in order to populate our reducer with fetched data and show it in our first component, this has a payload of an array of contents. Search will be triggered on change of our input field, this will have a string payload containing the search string.
import { Action } from '#ngrx/store';
/** App Models **/
import { Content } from '../models/content.model';
export const LOAD = '[Search] Load';
export const LOAD_SUCCESS = '[Search] Load Success';
export const SEARCH = '[Search] Search';
export class LoadAction implements Action {
readonly type = LOAD;
constructor() { }
}
export class LoadActionSuccess implements Action {
readonly type = LOAD_SUCCESS;
constructor(public payload: Content[]) { }
}
export class SearchAction implements Action {
readonly type = SEARCH;
constructor(public payload: string) {}
}
export type All
= LoadAction
| LoadActionSuccess
| SearchAction;
SearchEffect that will just fetch contents from api:
import { Injectable } from '#angular/core';
import { Actions, Effect } from '#ngrx/effects';
/** rxjs **/
import {of} from 'rxjs/observable/of';
import {map} from 'rxjs/operators/map';
import {mergeMap} from 'rxjs/operators/mergeMap';
import {catchError} from 'rxjs/operators/catchError';
/** ngrx **/
import * as searchActions from '../actions/search.actions';
/** App Services **/
import { SomeService } from '../services/some.service';
/** App Model **/
import {Content} from '../models/content.model';
#Injectable()
export class SearchEffects {
#Effect() load$ = this.actions$
.ofType(searchActions.LOAD)
.pipe(
mergeMap(() => {
return this.someService.getContentsFromApi()
.pipe(
map((contents: Content[]) => {
return new searchActions.LoadActionSuccess(contents);
}),
catchError(() => {
// do something
})
);
})
)
;
constructor(private someService: SomeService, private actions$: Actions) { }
}
SearchReducer will handle LoadSuccess when we successfully fetch contents from api and Search action that will filter our fetched contents to return only the ones containing our search string inside content's title parameter. We save first fetched contents in both of contents and searchedContents. Then, on search, we will update searchedContents to contain only contents having content.title including the searched string.
import { isEmpty } from 'lodash';
/** ngrx **/
import {createFeatureSelector} from '#ngrx/store';
import {createSelector} from '#ngrx/store';
/** App Models **/
import { Content } from '../models/content.model';
/** ngrx **/
import * as searchActions from '../actions/search.actions';
export type Action = searchActions.All;
export interface SearchsState {
contents: Content[];
searchedContents: Content[];
}
export const initialState: SearchsState = {
contents: [],
searchedContents: []
};
/ -------------------------------------------------------------------
// Selectors
// -------------------------------------------------------------------
export const selectContents = createFeatureSelector<SearchsState>('search');
export const getSearchedContents = createSelector(selectContents, (state: searchedContents) => {
return state.searchedContents;
});
export function contentsReducer(state: searchedContents = initialState, action: Action): searchedContents {
switch (action.type) {
case contentsActions.LOAD_SUCCESS:
const loadContents = action.payload.map(content => new Content(content));
return {
contents: loadContents,
searchedContents: loadContents
};
case contentsActions.SEARCH:
const keywordContents = isEmpty(action.payload) ? state.contents :
state.contents.filter(content => content.title.includes(action.payload));
return {
contents : state.contents,
searchedContents : keywordContents
};
default: {
return state;
}
}
}
So, updating searchedContents will automatically update the contents in our component.

ngrx store is the part of how you store the data. ngrx store is observable so your application flow is
Container -> Components
Container - wrapper component that will select data from store.
example:
const contacts$: Observable<contact> = this.store.pluck('contacts');
//*contacts$ - the dollar since is convention for Observable *//
Component - data visualization component, the data will be as Input(). example:
Input() contacts: Array<contact>;
this convention is called sometime SmartComponent(Container) and
DumbComponent(component)
now for a data transform/mapping you can use reactive approach(Rxjs) or functional programming or whatever you want but it not related for ngrx because in your contacts component the data as exist.
DEMO FOR YOUR SCENARIO:
contacts.container.ts
#Component({
selector: 'contacts-container',
template: `
<contacts-list [contacts]="contacts$ | async"></contacts-list>
`
})
export class ContactsContainer {
contacts$: Observable<[]contact> = this.store.pluck('contacts');
constructor(
private store: Store<applicationState>
) { }
}
contact-list.component.ts
#Component({
selector: 'contacts-list',
template: `
<input type="text" placeholder="write query" #query>
<ul>
<li *ngFor="contact of contacts | searchPipe: query.target.value">
</li>
</ul
`
})
export class ContactsListComponent {
contcats: Array<contact> = [];
constructor() { }
}
i use searchPipe for data transform ( custom pipe ) but is only example for data transform you can do it else.
Good Luck!

Related

Stencil JS - How to also distribute a set of shared methods

I have this simple component that is compiled to a web-component by Stencil:
import { Component, h, Prop } from "#stencil/core";
import { IAuthLoginConfig } from "../../interfaces";
import { initAuth } from "../../services";
#Component({
tag: "login-button",
styleUrl: "login-button.css",
})
export class LoginButton {
#Prop() public baseUrl: string = "/oidc/v1/authorize";
#Prop() public config: IAuthLoginConfig;
render() {
return (
<button onClick={() => initAuth(this.config, this.baseUrl)}>
Sign in
</button>
);
}
}
On the button click a shared function initAuth(...) is called that this is imported from the services-directory:
import { IAuthLoginConfig } from "../interfaces";
export const initAuth = (authConfig: IAuthLoginConfig, baseUrl: string): void => {
const url = `${baseUrl}${buildParameters(authConfig)}`;
window.location.href = url
};
const buildParameters = ({ oauth2, oidc }: IAuthLoginConfig) => {
return 'some parameters';
};
Is there any (standard Stencil) way to also build and publish this file so that a user of our web-component library can import the exported functions and use/call them? In our use-case an end-user should be able to use methods in his/her own application directly that are also used in our web-components.
Other use-cases: shared variables, classes...
Thanks in advance!
You will have to manually export each object you want to be accessible in ./src/index.ts, e.g.:
export { initAuth } from './services';
// or
export * from './services';
This allows you to import it in the consuming project:
import { initAuth } from 'your-installed-package';
// or (depending on how you published)
import { initAuth } from 'your-installed-package/dist';

Property 'map' does not exist on type '{}' rxjs6

Please view the following code:
import { Component } from '#angular/core';
import { AngularFirestore, AngularFirestoreCollection } from 'angularfire2/firestore';
import { Observable} from 'rxjs';
import { map } from 'rxjs/operators';
import { StudentsInfo } from '../studentInfo';
#Component({
selector: 'app-delete-student',
templateUrl: './delete-student.component.html',
styleUrls: ['./delete-student.component.css']
})
export class DeleteStudentComponent {
public itemsCollection: AngularFirestoreCollection<StudentsInfo>;
public items: Observable<StudentsInfo[]>;
constructor(private db: AngularFirestore) {
this.itemsCollection = this.db.collection<StudentsInfo>('/Stud_Info');
//this.items = this.itemsCollection.valueChanges();
// .snapshotChanges() returns a DocumentChangeAction[], which contains
// a lot of information about "what happened" with each change. If you want to
// get the data and the id use the map operator.
this.items = this.itemsCollection.snapshotChanges().pipe(
map(changes => changes.map(a => {
const data = a.payload.doc.data() as StudentsInfo;
const id = a.payload.doc.id;
return { id, ...data}
})
));
}
// Deleting a Student from Firestore
public deleteStudent(docId:string) {
this.itemsCollection.doc(docId).delete();
}
}
Problem: Property 'map' does not exist on type '{}'.
I have followed this documentation of angularfire2.
I have done everything according to the documentation needed and the according to the latest rxjs6 release.
The final angular application Runs fine but while doing ng build --prod --aot this error occurs.

retrieve data from firebase in ionic 3 and angular 5

Pls I need help in retrieving user data from firebase with AngularFireObject on logging in.
I have been able to save data to firebase but having issues retrieving it. Pls someone help out.
Many thanks in advance.
Ok, first you've to configure your AngularFireModule (I think you already do that). AngularFireModule.initializeApp(FIREBASE_CONFIG).
So, is a good way to create a model/service to handle your entities requests with firebase, something like this:
Model:
export interface Cutom {
key?: string,
name: string,
quantity: number,
price: number
}
Service:
import { Injectable } from '#angular/core';
import { AngularFireDatabase } from 'angularfire2/database';
import { Custom } from './../../models/custom.model';
#Injectable()
export class CustomService {
private customListRef;
constructor(private db: AngularFireDatabase) {
this.customListRef = this.db.list<Custom>('your-endpoint-at-firebase');
}
getCustomList() {
return this.customListRef;
}
}
In Component you will use your recently created service:
...
export class HomePage implements OnInit {
// remember to import the correct model
customList$: Observable<Custom[]>
constructor(
public navCtrl: NavController,
private customService: CustomListService
) {}
ngOnInit() {
this.customList$ = this.customService
.getCustomList()
.valueChanges();
}
or if you need the metadata too (like the ID):
ngOnInit() {
this.customList$ = this.customService
.getCustomList()
.snapshotChanges()
.pipe(
map(items => { // this needs to be imported with: import { map } from 'rxjs/operators';
return items.map(a => {
const data = a.payload.val();
const key = a.payload.key;
return {key, ...data};
});
}));
}
...
And the finally at your template:
<ion-item *ngFor="let item of customList$ | async">
{{item.name}}
</ion-item>
I hope it helps.

Return firebase values from a service to a component angular 6

I'm creating an application with angular 6 and firebase using angularfire2, I chose to use the firestore where I have a collection called pages like in the image:
basically I created a service - "PagesService" where I have a function that returns the data of the page that I sent. I'm trying to use getPage to return the values to my component, and assign them to the form, nothing else I tried worked, only returns an "observable" that I can not work, does anyone have an idea of what I can do?
Full code, service:
import { Injectable } from '#angular/core';
import { AngularFirestore, AngularFirestoreCollection, AngularFirestoreDocument } from 'angularfire2/firestore';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
#Injectable()
export class PagesService {
private pagesCollection: AngularFirestoreCollection<any>;
private page: AngularFirestoreDocument<any>;
constructor(private afs: AngularFirestore) {
this.pagesCollection = afs.collection('pages');
}
getPage(pageName: string) {
return this.afs.doc<any>('pages/${pageName}').valueChanges();
}
addPages(pageName: string, pageForm: any) {
this.pagesCollection.doc(pageName).set(pageForm.value);
}
}
My component:
import { Component, OnInit } from '#angular/core';
import { FormBuilder, FormGroup } from '#angular/forms';
import { Observable } from 'rxjs';
import { PagesService } from '../../services/pages.service';
#Component({
selector: 'app-quem-somos',
templateUrl: './quem-somos.component.html',
styleUrls: ['./quem-somos.component.scss']
})
export class QuemSomosComponent implements OnInit {
pageForm: FormGroup;
pageName: string = "wo-we-are";
page: any;
constructor(private pagesService: PagesService, private fb: FormBuilder) { }
ngOnInit() {
this.page = this.pagesService.getPage(this.pageName);
console.log(this.page);
this.pageForm = this.fb.group({
title: '',
content: ''
});
}
save() {
this.pagesService.addPages(this.pageName, this.pageForm);
}
}
obs: Sorry my english
If I have understand you right, When you say "Observable that I cannot work" is mean that you cannot access his data when you are trying to assign its values in the form?
In this case (I assume that your service is working as expected), just subscribe to it and populate the form after your values are ready to use. for example:
ngOnInit() {
this.pagesService.getPage(this.pageName).subscribe(v => {
// Here your data is ready, so you can send it to a function and populate the form as you need.
this.populateForm(v);
});
// Here I just construct the FormGroup, so your application can rendered.
this.pageForm = this.fb.group({
title: '',
content: ''
});
}
And add this function to do the task:
populateForm = (data) => {
console.log(data); // Just log it in console, and see if its the data that you seek for
}
Instead of console.log() you can populate your form or do what ever you need to.
Good Luck !
--EDIT--
I just noticed now, In your service:
getPage(pageName: string) {
return this.afs.doc<any>('pages/${pageName}').valueChanges();
}
You call the doc with ' ' instead of ``, so In fact, you are not using Template Strings. So your call is wrong and not fetch with the right path.
Change it to:
return this.afs.doc<any>(`pages/${pageName}`).valueChanges();

Get list as array from Firebase with angularfire2(v5) and ngrx effects

I have to tell you I'm getting crazy with it. I'm trying to get data from Firebase with AngularFire2(v.5) then work with it on #ngrx/effects and store it on #ngrx/store. Well, as I need the data with the keys, my code of effects looks like this:
spaces.effects.ts
#Effect()
getSpaces$ = this.actions$.ofType(SpacesActions.GET_SPACES_REQUEST)
.switchMap((action: SpacesActions.GetSpacesRequest) => {
return this.afs.list<Space>('/spaces').snapshotChanges()
.switchMap(actions => {
console.log('action is ', actions);
return actions.map(space => {
const $key = space.payload.key;
const data: Space = { $key, ...space.payload.val() };
console.log('snapshot is: ', data);
return new SpacesActions.GetSpacesSuccess(data);
});
}
);
My "actions" comes with the data and the key, then I get the key for each item because then I could update and delete items easily. My database has 3 items with 3 keys. If I run this code and log it, first I can see all items in 1 array with their payloads and with the second log I see each payload as snapshot.
When I call GetSpacesSuccess, I'd like to send all snapshots I got (with key and item) then store it. The way I'm doing now dispatch this action 3 times and I can see only 2 items on the screen because the first one is overridden by the second one.
So, two questions: Is there any easier way to get the items from firebase with their keys then store them with #ngrx? If not, what am I doing wrong that my first item is being overridden and my action is being dispatched 3 times?
Please, I'm doing my best with it as I'm learning. Thank you!
spaces.reducers.ts
case SpacesActions.GET_SPACES_REQUEST:
return {
state,
spaces: null,
loading: true
};
case SpacesActions.GET_SPACES_SUCCESS:
return {
...state,
...action.payload,
spaces: [state, action.payload],
loading: false
};
spaces.actions.ts
export class GetSpacesRequest implements Action {
readonly type = GET_SPACES_REQUEST;
}
export class GetSpacesSuccess implements Action {
readonly type = GET_SPACES_SUCCESS;
constructor(public payload: Space) {} <<<<<HERE I'D LIKE TO GET THE FULL ARRAY WITH EACH KEY
}
Thanks #AndreaM16 for the most complete answer. I went through the night working on it and I ended up doing it different. Actually, in the learning process we make mistakes in order to get the knowledge. Probably your solution is better than mine and I'll study that, thanks. Please, if possible, I'd love to hear your comments about my solution.
Finally, after reading lots of documentation, my effects is now this one, I don't have any error catcher though:
private spacesList = 'spaces/';
#Effect()
getSpaces$ = this.actions$.ofType(SpacesActions.GET_SPACES_REQUEST)
.switchMap(payload => this.afs.list(this.spacesList).snapshotChanges()
.map(spaces => {
return spaces.map(
res => {
const $key = res.payload.key;
const space: Space = {$key, ...res.payload.val()};
return space;
}
);
})
.map(res =>
new SpacesActions.GetSpacesSuccess(res)
));
Reducer
case SpacesActions.GET_SPACES_REQUEST:
return Object.assign({}, state, {
spaces: null,
loading: true
});
case SpacesActions.GET_SPACES_SUCCESS:
return Object.assign({}, state, {
spaces: action.payload,
loading: false
});
Actions
export class GetSpacesRequest implements Action {
readonly type = GET_SPACES_REQUEST;
}
export class GetSpacesSuccess implements Action {
readonly type = GET_SPACES_SUCCESS;
constructor(public payload: Space[]) {}
}
And, in my component, where I need the list:
constructor(private store: Store<fromSpaces.FeatureState>) {}
ngOnInit() {
this.store.dispatch(new SpacesActions.GetSpacesRequest());
this.spacesState = this.store.select('spaces');
}
If I understood your question correctly, you would like to store for each Item also store its key. You are looking for Map.
I would structure your feature as follows.
spaces.actions.ts:
Loading spaces requires no payload, while success has only an array of Space. I think you should build your Map<string,Space> in your reducer (string is your key).
import { Action } from '#ngrx/store';
/** App Models **/
import { Space } from './space.model';
export const GET_SPACES = '[Spaces] Spaces get';
export const GET_SPACES_SUCCESS = '[Start] Spaces get - Success';
export class GetSpacesAction implements Action {
readonly type = GET_SPACES;
}
export class GetSpacesActionSuccess implements Action {
readonly type = GET_SPACES_SUCCESS;
constructor(public payload: Space[]) {}
}
export type All
= GetSpacesAction
| GetSpacesActionSuccess;
spaces.effects.ts:
I'm assuming you just need a method to fetch spaces. If you need to do other stuff, just edit this piece of code. spaceService.getSpaces() is supposed to return only an array of Spaces. So, create a new Space model and, on your service, map each json entry to a new Space().
import { Injectable } from '#angular/core';
import { Actions, Effect } from '#ngrx/effects';
/** rxjs **/
import {map} from 'rxjs/operators/map';
import {mergeMap} from 'rxjs/operators/mergeMap';
import {catchError} from 'rxjs/operators/catchError';
/** ngrx **/
import * as spacesActions from './spaces.actions';
/** App Services **/
import { SpacesService } from './spaces.service';
#Injectable()
export class SpacesEffects {
#Effect() getSpaces$ = this.actions$
.ofType(spaceActions.GET_SPACES)
.pipe(
mergeMap(() => {
return this.spaceService.getSpaces()
.pipe(
map((spaces) => {
return new spacesActions.GetSpacesActionSuccess(spaces);
}),
catchError((error: Error) => {
// Handle erro here
})
);
})
)
;
constructor(private spacesService: SpacesService, private actions$: Actions) { }
}
spaces.reducer.ts
Here you build your map and you can also create a new action to return, for instance, a space given its key. I dont think you need any loading parameter here, I guess you are using it for some loading handling in your views, just use AsyncPipe in your view and handle a loading animation with an *ngIf checking if there are spaces or not.
/** ngrx **/
import {createFeatureSelector} from '#ngrx/store';
import {createSelector} from '#ngrx/store';
import * as spacesActions from './spaces.actions';
export type Action = spacesActions.All;
/** App Models **/
import { Space } from './space.model';
export interface SpaceState {
keySpaces: Map<string, Space>;
spaces: Space[];
keys: string[];
}
export const initialState: SpaceState = {
keySpaces: new Map<string, Space>(),
spaces: [],
keys: []
};
// Selectors
export const selectSpace = createFeatureSelector<SpaceState>('space');
export const getKeySpaces = createSelector(selectSpace, (state: StartState) => {
return state.keySpaces;
});
export const getSpaces = createSelector(selectSpace, (state: SpaceState) => {
return state.spaces;
});
export const getKeys = createSelector(selectSpace, (state: SpaceState) => {
return state.keys;
});
export function spacesReducer(state: SpaceState = initialState, action: Action): SpaceState {
switch (action.type) {
case startActions.GET_SPACES_SUCCESS:
// Since we return this from effect
const fetchedSpaces = action.payload;
const fetchedKeys = [];
const keySpacesMap = new Map<string, Space>();
fetchedSpaces.forEach( (space: Space) => {
fetchedkeys = fetchedKeys.concat(space.key);
keySpacesMap.set(space.key, new Space(space));
}
returns {
...state,
keySpaces: keySpacesMap,
spaces: fetchedSpaces,
keys: fetchedkeys
}
default: {
return state;
}
}
}
And, finally, you should be able to get such parameters in your components like:
. . .
keySpaces$ = Observable<Map<string, Space>>;
spaces$ = Observable<Array<Space>>;
keys$ = Observable<Array<string>>;
constructor(private _store: Store<AppState>) {
this.keySpaces$ = this._store.select(getKeySpaces);
this.space$s = this._store.select(getSpaces);
this.keys$ = this._store.select(getKeys);
}
. . .
ngOnInit() {
this._store.dispatch(new spacesActions.GetSpacesAction);
}
. . .
Of course add the new state to AppState:
. . .
export interface AppState {
. . .
space: SpaceState;
}

Resources