I create own store
export interface IAppState {
cartData:ICartItem[];
lastUpdate:Date;
}
and add some reducer to handle ADD and REMOVE actions
But in many places I just need latest data from store. According to documentation it is enough to add attr #select() to store item add you will get current state.
But I've created some service which will do all work like get, add and remove items from store
#Injectable()
export class CartService {
#select() private cartData: ICartItem[] ;
constructor(private ngRedux: NgRedux<IAppState>) { }
getItems() {
return this.cartData;
}
addItem(item: IItem) {
this.ngRedux.dispatch({type: ADD_CART_ITEM, cartItem : item});
}
removeItem(itemId: string) {
this.ngRedux.dispatch({type: REMOVE_CART_ITEM, id: itemId});
}
removeAllItems() {
this.ngRedux.dispatch({type: REMOVE_ALL_CART_ITEMS});
}
}
But problem - when I init myCartData property with getItems on init of my component, later I can add or remove some item, but myCartData property will not be updated after all this.
So how can I get latest state from store using such service? Or this approach is bad and I need to get state from store directly when I want without any custom services?
Try this:
#Injectable()
export class CartService {
#select('cartData') cartData$: Observable<ICartItem[]> ;
constructor(private ngRedux: NgRedux<IAppState>) { }
addItem(item: IItem) {
this.ngRedux.dispatch({type: ADD_CART_ITEM, cartItem : item});
}
removeItem(itemId: string) {
this.ngRedux.dispatch({type: REMOVE_CART_ITEM, id: itemId});
}
removeAllItems() {
this.ngRedux.dispatch({type: REMOVE_ALL_CART_ITEMS});
}
}
In your component file just subscribe to cardService.cardData$, or use an async pipe in your template.
Related
In my project, I wanted to create some sort of "Recommended Products" in each product page,
but having trouble with making my function filtering an observable.
I have tried using .pipe(filter()) in different ways, but to no use.
Basically the fucntion should filter products with the same type and id, and show them in the proper product page, but pretty much got stuck after subscribing all of my products(which is marked down below).
Much Appreciated!
import { Component, OnInit } from '#angular/core';
import { ProductService } from '../services/product.service';
import { ActivatedRoute, ParamMap, Router } from '#angular/router';
import Product from '../interfaces/product';
import { map, filter} from 'rxjs/operators';
import { Observable } from 'rxjs';
#Component({
selector: 'app-product',
templateUrl: './product.component.html',
styleUrls: ['./product.component.css']
})
export class ProductComponent implements OnInit {
recommandedProducts: Product[];
allProducts:Product[];
// allProducts:Observable< Product> = new Observable< Product>();
product: Product;
constructor(private productService: ProductService, private route: Router, private actRoute: ActivatedRoute) { }
ngOnInit() {
this.findRecommendedProducts(this.product)
};
//From ProductService:
// getProducts(){
// return this.http.get(`${this.uri}`);
// }
findRecommendedProducts(currectProduct: Product){
this.productService.getProducts().subscribe((data: Product[]) => {
this.allProducts = data;
console.log(this.allProducts)
this.recommandedProducts = this.allProducts.filter(otherProduct =>
otherProduct.type == currectProduct.type && otherProduct.id == currectProduct.id)
console.log(this.recommandedProducts);
});
};
}
A filter in rxjs is not the same as an Array.filter. In rxjs, a filter is used to emit values that pass the provided condition. So if you use a filter, based on the condition, the observable will either emit your data or return nothing.
Instead, what you need is pipe(map) along with an Array.filter. Also, as #jzzfs pointed out, your error shows currentProduct could be undefined, so you can pass a default value in your findRecommendedProducts function.
Try the below code.
findRecommendedProducts(currectProduct: Product = {} as Product) {
this.productService.getProducts()
.pipe(
map((products: Product[]) => products.filter(product => product.type == currectProduct.type && product.id == currectProduct.id))
)
.subscribe((data: Product[]) => {
this.recommandedProducts = data;
});
};
Now your subscribed data should directly return the recommendedProducts.
Looks like the currectProduct passed onto findRecommendedProducts is undefined, since the logged this.allProducts do contain values.
With that being said, notice that when you define product: Product, you've only defined its type but you have not initialized it. The default value is therefore undefined -- if I remember correctly.
So change product: Product to product: Product = {};, for instance or pass a value to it within the constructor or within ngInit.
I'm trying to create a user profile service for an Angular 4 project and struggling a little with how to properly initialize and update the observable Profile object. Currently, when the user authenticates (via Firebase), AuthService passes the user's auth info to UserProfileService via the latter's initialize() function. UserProfileService then looks up the user's profile (or creates one if none exists yet) and populates a public observable with the profile.
The problem I'm running into is with other parts of the application trying to subscribe to the profile observable before all this has happened. I'd originally been initializing the observable via ...
public profileObservable: UserProfile = null;
... which of course resulted in a "subscribe() does not exist on null" error, so I changed it to ...
public profileObservable: Observable<UserProfile> = Observable.of();
This at least doesn't throw any errors, but anything that subscribes to profileObservable before I've mapped the Firebase object to it never updates.
Complete code for user-profile.service.ts below. I'm still struggling to get my head around how some of this is meant to work, so hopefully someone can shed some light. Thanks!
import { Injectable } from '#angular/core';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/observable/of';
import { FirebaseListObservable, FirebaseObjectObservable, AngularFireDatabase } from 'angularfire2/database';
import * as firebase from 'firebase/app';
export class UserProfile {
$exists: Function;
display_name: string;
created_at: Date;
}
#Injectable()
export class UserProfileService {
private basePath: string = '/user-profiles';
private profileRef: FirebaseObjectObservable<UserProfile>;
public profileObservable: Observable<UserProfile> = Observable.of();
constructor(private db: AngularFireDatabase) {
// This subscription will never return anything
this.profileObservable.subscribe(x => console.log(x));
}
initialize(auth) {
this.profileRef = this.db.object(`${this.basePath}/${auth.uid}`);
const subscription = this.profileRef.subscribe(profile => {
if (!profile.$exists()) {
this.profileRef.update({
display_name: auth.displayName || auth.email,
created_at: new Date().toString(),
});
} else subscription.unsubscribe();
});
this.profileObservable = this.profileRef.map(profile => profile);
// This subscription will return the profile once it's retrieved (and any updates)
this.profileObservable.subscribe(profile => console.log(profile));
}
};
You must not change observable references once you constructed them. The way I found to properly decouple subscribers from the datasource is to use an intermediate Subject, which is both an observer and an observable.
Your code would look something like this:
import { Observable } from 'rxjs/Observable';
import { Subject } from 'rxjs/Subject';
...
export class UserProfileService {
...
public profileObservable = new Subject<UserProfile>();
constructor(private db: AngularFireDatabase) {
// This subscription now works
this.profileObservable.subscribe(x => console.log(x));
}
initialize(auth) {
const profileRef = this.db.object(`${this.basePath}/${auth.uid}`);
...
profileRef.subscribe(this.profileObservable);
}
};
I am trying to get the index of a dynamically created component inside ViewContainerRef
I need to get the index so I can destroy the component if I wanted too.
Code Below
#ViewChild('dynamicInsert', { read: ViewContainerRef }) dynamicInsert: ViewContainerRef
componentFactory
constructor(
private componentFactoryResolver: ComponentFactoryResolver,
private viewContainerRef: ViewContainerRef,
) { }
ngAfterViewInit() {
this.componentFactory = this.componentFactoryResolver.resolveComponentFactory(AssetOptionComponent);
}
addAssetOption() {
const dynamicComponent = <AssetOptionComponent>this.dynamicInsert.createComponent(this.componentFactory).instance
// how to get index of this dynamically generated component ^^^^
}
Trying to use
this.dynamicInsert.remove(index: number) to destroy component
but I first need the index of the dynamically created component
this.dynamicInsert.indexOf(viewRef: viewRef)
To get the index you can use indexOf method and hostView property:
const index = this.dynamicInsert.indexOf(dynamicComponent.hostView)
Also note that if you don't specify the index view container will destroy the last component:
remove(index?: number): void {
const viewData = detachEmbeddedView(this._data, index);
if (viewData) {
Services.destroyView(viewData);
}
}
export function detachEmbeddedView(elementData: ElementData, viewIndex?: number): ViewData|null {
const embeddedViews = elementData.viewContainer !._embeddedViews;
if (viewIndex == null || viewIndex >= embeddedViews.length) {
viewIndex = embeddedViews.length - 1;
}
So if you have only one component you don't need to pass index.
To remove all components you can use clear method.
If you are looking to destroy the created component you may consider a shortcut by just subscribing to it's observable destroy:
addAssetOption() {
const dynamicComponent: ComponentRef<any> = this.dynamicInsert.createComponent(this.componentFactory);
dynamicComponent.instance.destroy.subscribe(() => dynamicComponent.destroy())
}
and then upon removing event, in AssetOptionComponent, call it:
export class AssetOptionComponent {
destroy = new Destroyable();
delete(){
this.destroy.delete();
}
}
export class Destroyable extends Subject<any>{
delete() {
this.next();
}
}
Working demo
I'm using DevExpress' DevExtreme with Angular2. I have a data grid (below) that lists states and asks the user to select some states. It is possible that some states have already been stored in the database. How do I set the previously selected states? I can see in the documentation that I should use dataGrid.instance.selectRows(arrayOfPreviouslySelectedStates) but dataGrid is instantiated sometime after I try to set it which is in the ngOnInit().
My HTML grid:
<dx-data-grid #statesGrid id="statesContainer" [dataSource]="states" [selectedRowKeys]="[]" [hoverStateEnabled]="true" [showBorders]="true" [showColumnLines]="true" [showRowLines]="true" [rowAlternationEnabled]="true">
<dxo-sorting mode="multiple"></dxo-sorting>
<dxo-selection mode="multiple" [deferred]="true"></dxo-selection>
<dxo-paging [pageSize]="10"></dxo-paging>
<dxo-pager [showPageSizeSelector]="true" [allowedPageSizes]="[5, 10, 20]" [showInfo]="true"></dxo-pager>
<dxo-filter-row [visible]="true"></dxo-filter-row>
<dxi-column dataField="abbreviation" [width]="100"></dxi-column>
<dxi-column dataField="name"></dxi-column>
</dx-data-grid>
My componenet:
import 'rxjs/add/operator/switchMap';
import { Component, OnInit, ViewContainerRef, ViewChild } from '#angular/core';
import { CompanyService } from './../../../shared/services/company.service';
import { StateService } from './../../../shared/services/state.service';
import notify from 'devextreme/ui/notify';
import { DxDataGridModule, DxDataGridComponent } from 'devextreme-angular';
#Component({
selector: 'app-company-detail',
templateUrl: './company-detail.component.html'
})
export class CompanyDetailComponent implements OnInit {
#ViewChild(DxDataGridComponent) dataGrid: DxDataGridComponent;
companyStates: Array<ICompanyState>;
states: Array<IState>;
constructor(private CompanyService: CompanyService, private StateService: StateService) { }
ngOnInit() {
this.StateService.getStates().subscribe((states) => {
this.getSelectedStates();
this.states = states
});
}
public getSelectedStates = (): void => {
this.CompanyService.getStates(id).subscribe((states) => {
let preselectedStates: Array<IState> = this.companyStates.map((state) => {
return { abbreviation: state.Abbreviation, name: state.Name }
});
// I get an error here that says that dataGrid is undefined.
this.dataGrid.instance.selectRows(preselectedStates, false);
}
}
}
Thanks to #yurzui 's comment I was able to figure out my problems in the following way. [selectedRowKeys] deals with all preselection. It's "problem" is that it doesn't update itself when additional selections are made. So, I listened for onSelectionChanged and passed the event, which contains data about many things regarding selection, into my custom function which updates the selectedStates which I then use to save the data to the database when the save button is clicked.
Gets the preselected states from the database
public getCompanyStates = (): void => {
this.CompanyService.getStates().subscribe((states) => {
this.selectedStates = states;
});
}
Event handler
public onSelectionChanged = (e): void => {
this.selectedStates = e.selectedRowKeys;
}
The dx-data-grid portion of the HTML
<dx-data-grid #statesGrid id="statesContainer"
(onSelectionChanged)="onSelectionChanged($event)"
[selectedRowKeys]="selectedStates"
[dataSource]="states">
...
</dx-data-grid>
I have 2 components: CommandListComponent and CommandLineComponent. Inside of a CommandListComponent template i handle a click event on a text string:
CommandListComponent template:
<li *ngFor="#command of commandList" class="b-command-list__command"><span (click)="checkCommand(command)" class="b-command-list__text">{{command}}</span></li>
commandlist.component.ts
import {CommandLineComponent} from "./commandline.component";
...
export class CommandListComponent {
commandLineComponent: any;
constructor(private _commandLine: CommandLineComponent) {
this.commandLineComponent = _commandLine;
}
checkCommand(command: string): void {
this.commandLineComponent.add(command);
}
}
When click is fired i pass choosen command to add method of a CommandLineComponent:
export class CommandLineComponent {
commands: string[] = [];
add(command: string): void {
if (command) this.commands.push(command);
console.log(this.commands);
}
}
And within a template of a CommandLineComponent i print a list of a commands with *ngFor:
<li *ngFor="#command of commands" class="b-command-textarea__command">{{command}}</li>
But *ngFor doesn't fires when i choose a command and commands array of a CommandLineComponent updated. So, data binding is not working. commands array updates successfully:
Thank you for help.
The problem is the way you reference the commandLineComponent component. If there is a relation between them you could use the ViewChild decorator
class CommandListComponent {
#ViewChild(CommandLineComponent)
commandLineComponent: any;
(...)
}
If not, you need to use a shared service to share the commands list between these two components. Something like that:
export class CommandService {
commands:string[] = [];
commandAdded:Subject<string> = new Subject();
add(command: string): void {
if (command) {
this.commands.push(command);
this.commandAdded.next(command);
}
console.log(this.commands);
}
}
You need to define the service when bootstrapping your application and both components can inject it.
class CommandListComponent {
constructor(private commandService:CommandService) {
}
}
checkCommand(command: string): void {
this.commandService.add(command);
}
The CommandLineComponent component will be notified of a new command like this and can update the view accordingly:
class CommandLineComponent {
constructor(private commandService:CommandService) {
this.commandService.commandAdded.subscribe(command => {
// Update the list displayed in the component...
});
}
}