Integrate Ng-Survey multiple time inside the same component - javascript

Im using ng-surveys template inside my angular application
https://ng-surveys.firebaseapp.com/
I put the template selector "" inside *ngFor to make multiple surveys in the same page.
It works but the browser considers all the surveys as the same survey : whene I change something in one survey it changes in the other surveys.
I tried to integrate the template dynamicly using Angular ComponentResolver but I faced many errors and I'm not sure this way can fix my problem.
create-assignment.component.html:
<div class="row">
<div class="col-md-12">
<div class="card">
<div class="card-header">
<i class="fa fa-file-text-o fa-lg"></i> Questionnaires
</div>
<div class="card-body">
<tabset>
<tab *ngFor="let survey of surveys; let i = index">
<ng-template tabHeading><img [src]="survey.category_icon"/> {{survey.category_name}}</ng-template>
<!-- <app-create-survey></app-create-survey> -->
<!-- <ng-template #dynamic></ng-template> -->
<ngs-builder-viewer [options]="options"></ngs-builder-viewer>
</tab>
</tabset>
</div>
</div>
</div>
</div>
create-assignment.component.ts:
import { Component, OnInit, ViewChild, ViewContainerRef, Inject, AfterViewInit, AfterViewChecked } from '#angular/core';
import { LocalStoreService } from '../../../shared/services/local-store.service';
import { ApplicationService } from '../../../shared/services/application.service';
import { NgbDateParserFormatterService } from '../../../shared/services/ngb-date-parser-formatter.service';
import { Router } from '#angular/router';
import { ToastrService } from 'ngx-toastr';
import { IBuilderOptions, NgSurveyState, IElementAndOptionAnswers } from '../../../shared/ng-surveys/models';
import { Observable } from 'rxjs';
import { Survey } from '../../../shared/models/survey';
import { LoaderService } from '../../../shared/services/loader.service';
#Component({
selector: 'app-create-assignment',
templateUrl: './create-assignment.component.html',
styleUrls: ['./create-assignment.component.scss']
})
export class CreateAssignmentComponent implements OnInit, AfterViewChecked {
options: IBuilderOptions;
assignment: any = {};
pending: Boolean = false;
user: any;
shops: any = [];
survey_categories: any = [];
surveys: any = [];
errors: any = {};
service: any
#ViewChild('dynamic', {
read: ViewContainerRef , static: false
}) viewContainerRef: ViewContainerRef
constructor(
#Inject(LoaderService) service,
private ls: LocalStoreService,
public router: Router,
public toastr: ToastrService,
private dateService: NgbDateParserFormatterService,
private appServ: ApplicationService
) {
this.service = service
}
ngAfterViewChecked() {
// this.service.setRootViewContainerRef(this.viewContainerRef)
// this.service.addDynamicComponent()
}
ngOnInit() {
this.options = {
importSurvey: {
callback: this.importSurvey.bind(this),
},
surveyButtons: [{
title: 'Sauvegarder questionnaire',
icon: 'i-Data-Save',
text: 'Sauvegarder',
callback: this.saveSurvey.bind(this),
}],
importElement: {
callback: this.importElement.bind(this),
},
elementButtons: [{
title: 'Sauvegarder question',
icon: 'i-Data-Save',
text: 'Sauvegarder',
callback: this.saveElement.bind(this),
}]
};
this.appServ.getSurveyCategories().subscribe((response: any) => {
this.survey_categories = response.data;
this.survey_categories.forEach(el => {
this.surveys.push(new Survey(el.id, el.name, el.icon, this.options));
});
});
this.user = this.ls.getItem('user');
this.appServ.getBusinessShops(this.user).subscribe((response: any) => {
this.shops = response.data;
});
}
importSurvey(): Observable<NgSurveyState> {
// Mocking get request
return this.getSurvey();
}
importElement(): Observable<IElementAndOptionAnswers> {
// Mocking get request
return this.getElement();
}
getSurvey(): Observable<NgSurveyState> {
return
}
getElement(): Observable<IElementAndOptionAnswers> {
return
}
saveSurvey(ngSurveyState: NgSurveyState): void {
console.log(ngSurveyState);
}
saveElement(element: IElementAndOptionAnswers): void {
// Add post request to save survey data to the DB
console.log('element: ', element);
}
toObject = (map = new Map) =>
Array.from
( map.entries()
, ([ k, v ]) =>
v instanceof Map
? { key: k, value: this.toObject (v) }
: { key: k, value: v }
)
}
I want to make each survey not similar to other surveys

If you look into source code of that library:
In this file projects/ng-surveys/src/lib/ng-surveys.module.ts
These reducers(services) contain survey object:
NgSurveyStore, SurveyReducer, PagesReducer, ElementsReducer, OptionAnswersReducer, BuilderOptionsReducer provided under module level injectors if we can provide these in component level injectors data wouldnt be shared.
Since they are provided in module and when you import in your own module, they are sharing single instance of these reducers which store state survey object.

Related

In Angular why will my Dynamic Tabs / Tab Panes work initially but not after update?

I am currently using version 3.4 of CoreUI for my Angular App and I am attempting to use their Tabs setup.
Currently it works fine if I populate the tabs oninit but if I update them it breaks. The entire purpose of being able to use this is to have a dynamic list of tabs and content in those tabs so I am at a loss for why this wouldn't work.
I made a very dumbed down version below to show what I am trying. Note that the first instance of this._tabs works fine, after my await calls when I update to the second version it fails to display correctly.
HTML
<c-card>
<c-card-header>
Tabs <code><small>nav-underline</small></code>
</c-card-header>
<c-card-body>
<c-tabset class="nav-underline nav-underline-primary">
<c-tablist>
<c-tab *ngFor="let tab of tabs" >
{{tab.header}}
</c-tab>
</c-tablist>
<c-tab-content>
<br>
<c-tab-pane *ngFor="let tab of tabs">
<p>
{{tab.panel}}
</p>
</c-tab-pane>
</c-tab-content>
</c-tabset>
</c-card-body>
</c-card>
Component
import { Component, OnInit } from '#angular/core';
import { FormGroup } from '#angular/forms';
import { DbService, DashCpResult, IssuerGroup } from '../../db.service';
import { ActivatedRoute } from '#angular/router';
import { Router } from '#angular/router';
#Component({
selector: 'app-dashboard',
templateUrl: './dashboard.component.html',
styleUrls: ['./dashboard.component.scss']
})
export class DashboardComponent implements OnInit {
// Arrays
DashCpResults: Array<DashCpResult>;
private _setTab: number;
get setTab() {
return this._setTab;
}
set setTab(value: number) {
this._setTab = value || 0;
}
private _tabs: any[] = [];
public get tabs() {
return this._tabs;
}
constructor(
private DbService: DbService,
private route: ActivatedRoute,
private router: Router,
) {
this.setTab = 0;
}
rungetDashCpResult(id) {
return new Promise((resolve) => {
this.DbService.getDashCpResult(id).subscribe(DashCpResults => this.DashCpResults = DashCpResults,
(err) => {
console.log('ERROR: ' + JSON.stringify(err));
},() => {
resolve(1);
});
})
}
async ngOnInit() {
this._tabs = [
{ header: 'Home', panel: 'Home'},
{ header: 'Home2', panel: 'Home2'},
{ header: 'Home3', panel: 'Home3'},
{ header: 'Home4', panel: 'Home4'}
];
// Get Screen detail for Tabs
try { await this.rungetDashCpResult(1); } catch (e) {console.log(e)}
// Update Tabs for a test
this._tabs = [
{ header: 'Home5', panel: 'Home5'},
{ header: 'Home6', panel: 'Home6'},
{ header: 'Home7', panel: 'Home7'},
{ header: 'Home8', panel: 'Home8'}
];
}
}

Data not passing through with route in Angular 6

I have a application with a table of cars:
This is my code:
Carcomponent.html
<tbody>
<tr *ngFor="let car of allCars; index as carId" \>
<td [routerLink]="['/cars', carId]">{{car.carId}}</td>
<td>{{car.brand}}</td>
<td>{{car.model}}</td>
<td>{{car.color}}</td>
<td>{{car.topSpeed }}</td>
</tr>
</tbody>
I have register the route like this:
{ path: 'cars/:carId', component: CardetailsComponent }
And this is my CarDetails.ts file:
import { Component, OnInit } from '#angular/core';
import { ActivatedRoute } from '#angular/router';
import { CarVM } from '../viewmodels/car-vm';
import { CarService } from '../services/car.service';
#Component({
selector: 'app-cardetails',
templateUrl: './cardetails.component.html',
styleUrls: ['./cardetails.component.css']
})
export class CardetailsComponent implements OnInit {
car: any;
carList: any;
constructor(private route: ActivatedRoute, private carservice: CarService) { }
ngOnInit() {
this.route.paramMap.subscribe(params => {
this.car = params.get('carId');
});
}
getCarList() {
this.carList = new CarVM();
this.carservice.getCarById(this.carList.carId).subscribe((res: any) => {
this.carList = res.data;
console.log(this.carList)
})
}
}
And on my Cardetails.html I want to show the selected car like this:
<h2>Car Details</h2>
<div *ngIf="car">
<h3>{{ car.brand }}</h3>
<h4>{{ car.model }}</h4>
<p>{{ car.color }}</p>
</div>
The routing is working fine and fetching the cars is working. Now I want to select one car and see the brand, model, color on the next page. I use a viewmodel for this:
export class CarVM {
CarId: number;
Brand: string;
Model: string;
Color: string;
TopSpeed: number;
}
How can I see the selected car on the next page?
I have followed this tutorial:
https://angular.io/start/routing
Ok, you seem to be bit confused. In cardetails component you want to process carId from route parameters and use it to get car details. You can either get them from server, or have the service return already loaded details of all cars.
Let's say we are trying to make it happen getting the first way, it might look like this:
import { map, switchMap } from 'rxjs/operators';
ngOnInit() {
this.getCar();
}
private getCar(): void {
this.route.paramMap.pipe(
map(params => params.get('carId')),
switchMap(carId => {
return this.carservice.getCarById(carId);
})
).subscribe(
res => {
this.car = res;
console.log('#My car:', this.car);
}
);
}
First, you'll get the carId from route.paramMap, map it using rxjs map, then use switchMap to call you carservice.getCarById(carId) and have it return Observable to which you can subscribe. This should do the trick. Don't forget to properly map it/create CarVM object from it.
The problem is, you don't have CarVM object properly on CardetailsComponent. You are only getting carId into CarVM here: this.car = CarVM[+params.get('carId')];
First you need to create CarVM properly with your class variables. And the you can call your index.
import { Component, OnInit } from '#angular/core';
import { ActivatedRoute } from '#angular/router';
import { CarVM } from '../viewmodels/car-vm';
#Component({
selector: 'app-cardetails',
templateUrl: './cardetails.component.html',
styleUrls: ['./cardetails.component.css']
})
export class CardetailsComponent implements OnInit {
car: any;
carList: any;
constructor(private route: ActivatedRoute) { }
ngOnInit() {
this.route.paramMap.subscribe(params => {
this.car = params.get('carId');
});
}
getCarList(){
this.carList = new CarVM();
//call your service here to fill your carList variable and once you get car list, you will be able to access variable using with your index (this.car).
}
}

Need to set dynamic value for ngModel sent from input in angular 4

There are two address shipping and billing Country. Both has different value.
Components are reused here.
A select-country component was made for this .
<select-country [addressType]="'shipping'"></select-country>
<select-country [addressType]="'billing'"></select-country>
The type can be shipping or billing.
Now in select-country
import { Component, OnInit,ViewChild, ElementRef,Input } from '#angular/core';
import { ConfigService } from '../services/config.service';
import { DataService } from '../services/data.service';
import { CheckOutService } from '../services/checkout/check-out.service';
import { HomeService } from './../services/banner/home.service';
import {MdlService} from './../services/material-design-lite/mdl.service';
import { Http } from "#angular/http";
import { apiUrl,used_currency,used_language } from './../services/global.constant';
import { Router,ActivatedRoute } from '#angular/router';
import {NgSelectModule, NgOption} from '#ng-select/ng-select';
import {HttpClient, HttpClientModule} from '#angular/common/http';
import {Observable} from 'rxjs/Observable';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/distinctUntilChanged';
import 'rxjs/add/operator/debounceTime';
import 'rxjs/add/operator/switchMap';
declare var componentHandler: any;
#Component({
moduleId: module.id + '',
selector: 'select-country',
templateUrl: './get-country.component.html',
styleUrls: ['./get-country.component.css']
})
export class GetCountryComponent implements OnInit {
#Input('addressType') addressType;
searchQuery: string = '';
items;va;
countries = new Array;
constructor(public http: Http,public router: Router,
public configz: ConfigService,public shared: DataService,
private checkOutservice: CheckOutService ,
private service: HomeService, public material:MdlService) {
if(this.addressType=='shipping')
{va=shared.orderDetails.delivery_country}
else{va=shared.orderDetails.billing_country}
var data = { type: 'null' };
http.post(this.configz.url + 'getCountries', data).map(res => res.json()).subscribe(data => {
this.items = this.countries = data.data;
console.log(this.items);
setTimeout(() => { this.material.render(); }, 550);
});
}
ngAfterViewInit(){
}
ngOnInit() {
}
static mdlWrapper(element: ElementRef) {
componentHandler.upgradeElement(element.nativeElement);
}
}
<div tabindex="-1">
<select [(ngModel)]="shared.orderDetails.delivery_country" name="orderby" >
<option *ngFor="let item of items" value="" >{{ item.countries_name }}</option>
</select>
</div>
Shared is service which is shared among all component.
For shipping shared.orderDetails.delivery_country is used and for billing shared.orderDetails.billing_country
How to dynamically change ngModel and set shared.orderDetails .
I am making changes in shared only because there are multiple component and they need to share the same service to retain data.
EDIT : I tried setting a variable in Get Country Component. Edited it please check. It does not update the shared.orederDetails.
Why don't you build the <select-country> as a proper form control? That way you can use ngModel binding and forget about the details. Something like this:
<form>
<input [(ngModel)]="order.name" name="name">
<input [(ngModel)]="order.count" name="count">
<select-country [(ngModel)]="order.shippingCountry" name="shippingCountry"></select-country>
<select-country [(ngModel)]="order.billingCountry" name="billingCountry"></select-country>
</form>
The component you can build like this:
import { Component, Input, forwardRef, Output, EventEmitter, OnInit } from '#angular/core';
import { NG_VALUE_ACCESSOR, ControlValueAccessor } from '#angular/forms';
import { HttpClient } from '#angular/common/http';
interface Country {
value: string;
countryName: string;
}
const SELECT_COUNTRY_VALUE_ACCESSOR = {
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => SelectCountryComponent),
multi: true,
};
#Component({
selector: 'select-country',
template: `
<div tabindex="-1">
<select [(ngModel)]="value" name="orderby" (ngModelChange)="updateValue($event)">
<option *ngFor="let country of countries" [selected]="country.value === value?.value" [value]="country.value">{{ country.countryName }}</option>
</select>
</div>`,
styles: [`:host { display: inline-block; }`],
providers: [SELECT_COUNTRY_VALUE_ACCESSOR]
})
export class SelectCountryComponent implements ControlValueAccessor, OnInit {
_value: Country;
countries = []
private onChangeCallback: Function;
private onTouchedCallback: Function;
constructor(private http: HttpClient) {}
ngOnInit() {
this.http.get('https://www.mocky.io/v2/5a90341e2f000061006caba7')
.subscribe((countries: Country[]) => this.countries = countries,
err => console.error('Error getting countries:', err));
}
/** that mock should return something like this:
* [{
"countryName": "Ireland",
"value": "IRELAND" // or whatever
}, ...]
*/
// update coming from external, e.g. form control
writeValue(value) {
// outside change of selected item
console.log('Update value:', value);
this._value = value;
}
// update coming from the view (our dropdown), we update it and then go on to inform Angular Forms.
// called by the dropdown selector.
updateValue(value) {
this._value = this.countries.filter(country => country.value === value)[0];
this.valueChange();
}
private valueChange() {
this.onTouchedCallback();
this.onChangeCallback(this._value);
}
// these two are set
registerOnChange(onChangeCallback: Function) {
this.onChangeCallback = onChangeCallback;
}
registerOnTouched(onTouchedCallback: any) {
this.onTouchedCallback = onTouchedCallback;
}
}
Of course, adjust to oyur needs, add validation, styling etc. You can see it in action here: https://stackblitz.com/edit/angular-country-picker?file=app/app.component.html
In addition to my other answer (with proposal to make a lil' standalone component you could reuse elsewhere), let's try a more direct approach, fixing your code:
First, your template options all lack values. Even when your user picks something, ngModel doesn't change (because it's always the same value, "").
<div tabindex="-1">
<select [(ngModel)]="shared.orderDetails.delivery_country" name="orderby">
<option *ngFor="let item of items" [value]="item.countries_name">{{ item.countries_name }}</option>
</select>
</div>
Second, your init logic is in the constructor. At that instance, you still don't have your inputs settled. Move init parts to your ngOnInit.
constructor(public http: Http,
public router: Router,
public configz: ConfigService,
public shared: DataService,
private checkOutservice: CheckOutService,
private service: HomeService,
public material:MdlService) {
}
ngOnInit() {
if(this.addressType === 'shipping') {
this.va = shared.orderDetails.delivery_country
} else {
this.va = shared.orderDetails.billing_country;
}
const data = { type: 'null' };
http.post(this.configz.url + 'getCountries', data).map(res => res.json()).subscribe(data => {
this.items = this.countries = data.data;
console.log(this.items);
setTimeout(() => { this.material.render(); }, 550);
});
}

Angular 4: reactive form control is stuck in pending state with a custom async validator

I am building an Angular 4 app that requires the BriteVerify email validation on form fields in several components. I am trying to implement this validation as a custom async validator that I can use with reactive forms. Currently, I can get the API response, but the control status is stuck in pending state. I get no errors so I am a bit confused. Please tell me what I am doing wrong. Here is my code.
Component
import { Component,
OnInit } from '#angular/core';
import { FormBuilder,
FormGroup,
FormControl,
Validators } from '#angular/forms';
import { Router } from '#angular/router';
import { EmailValidationService } from '../services/email-validation.service';
import { CustomValidators } from '../utilities/custom-validators/custom-validators';
#Component({
templateUrl: './email-form.component.html',
styleUrls: ['./email-form.component.sass']
})
export class EmailFormComponent implements OnInit {
public emailForm: FormGroup;
public formSubmitted: Boolean;
public emailSent: Boolean;
constructor(
private router: Router,
private builder: FormBuilder,
private service: EmailValidationService
) { }
ngOnInit() {
this.formSubmitted = false;
this.emailForm = this.builder.group({
email: [ '', [ Validators.required ], [ CustomValidators.briteVerifyValidator(this.service) ] ]
});
}
get email() {
return this.emailForm.get('email');
}
// rest of logic
}
Validator class
import { AbstractControl } from '#angular/forms';
import { EmailValidationService } from '../../services/email-validation.service';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/observable/of';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/switchMap';
import 'rxjs/add/operator/debounceTime';
import 'rxjs/add/operator/distinctUntilChanged';
export class CustomValidators {
static briteVerifyValidator(service: EmailValidationService) {
return (control: AbstractControl) => {
if (!control.valueChanges) {
return Observable.of(null);
} else {
return control.valueChanges
.debounceTime(1000)
.distinctUntilChanged()
.switchMap(value => service.validateEmail(value))
.map(data => {
return data.status === 'invalid' ? { invalid: true } : null;
});
}
}
}
}
Service
import { Injectable } from '#angular/core';
import { HttpClient,
HttpParams } from '#angular/common/http';
interface EmailValidationResponse {
address: string,
account: string,
domain: string,
status: string,
connected: string,
disposable: boolean,
role_address: boolean,
error_code?: string,
error?: string,
duration: number
}
#Injectable()
export class EmailValidationService {
public emailValidationUrl = 'https://briteverifyendpoint.com';
constructor(
private http: HttpClient
) { }
validateEmail(value) {
let params = new HttpParams();
params = params.append('address', value);
return this.http.get<EmailValidationResponse>(this.emailValidationUrl, {
params: params
});
}
}
Template (just form)
<form class="email-form" [formGroup]="emailForm" (ngSubmit)="sendEmail()">
<div class="row">
<div class="col-md-12 col-sm-12 col-xs-12">
<fieldset class="form-group required" [ngClass]="{ 'has-error': email.invalid && formSubmitted }">
<div>{{ email.status }}</div>
<label class="control-label" for="email">Email</label>
<input class="form-control input-lg" name="email" id="email" formControlName="email">
<ng-container *ngIf="email.invalid && formSubmitted">
<i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Please enter valid email address.
</ng-container>
</fieldset>
<button type="submit" class="btn btn-primary btn-lg btn-block">Send</button>
</div>
</div>
</form>
There's a gotcha!
That is, your observable never completes...
This is happening because the observable never completes, so Angular does not know when to change the form status. So remember your observable must to complete.
You can accomplish this in many ways, for example, you can call the first() method, or if you are creating your own observable, you can call the complete method on the observer.
So you can use first()
UPDATE TO RXJS 6:
briteVerifyValidator(service: Service) {
return (control: AbstractControl) => {
if (!control.valueChanges) {
return of(null);
} else {
return control.valueChanges.pipe(
debounceTime(1000),
distinctUntilChanged(),
switchMap(value => service.getData(value)),
map(data => {
return data.status === 'invalid' ? { invalid: true } : null;
})
).pipe(first())
}
}
}
A slightly modified validator, i.e always returns error: STACKBLITZ
OLD:
.map(data => {
return data.status === 'invalid' ? { invalid: true } : null;
})
.first();
A slightly modified validator, i.e always returns error: STACKBLITZ
So what I did was to throw a 404 when the username was not taken and use the subscribe error path to resolve for null, and when I did get a response I resolved with an error. Another way would be to return a data property either filled width the username or empty
through the response object and use that insead of the 404
Ex.
In this example I bind (this) to be able to use my service inside the validator function
An extract of my component class ngOnInit()
//signup.component.ts
constructor(
private authService: AuthServic //this will be included with bind(this)
) {
ngOnInit() {
this.user = new FormGroup(
{
email: new FormControl("", Validators.required),
username: new FormControl(
"",
Validators.required,
CustomUserValidators.usernameUniqueValidator.bind(this) //the whole class
),
password: new FormControl("", Validators.required),
},
{ updateOn: "blur" });
}
An extract from my validator class
//user.validator.ts
...
static async usernameUniqueValidator(
control: FormControl
): Promise<ValidationErrors | null> {
let controlBind = this as any;
let authService = controlBind.authService as AuthService;
//I just added types to be able to get my functions as I type
return new Promise(resolve => {
if (control.value == "") {
resolve(null);
} else {
authService.checkUsername(control.value).subscribe(
() => {
resolve({
usernameExists: {
valid: false
}
});
},
() => {
resolve(null);
}
);
}
});
...
I've been doing it slightly differently and faced the same issue.
Here is my code and the fix in case if someone would need it:
forbiddenNames(control: FormControl): Promise<any> | Observable<any> {
const promise = new Promise<any>((resolve, reject) => {
setTimeout(() => {
if (control.value.toUpperCase() === 'TEST') {
resolve({'nameIsForbidden': true});
} else {
return null;//HERE YOU SHOULD RETURN resolve(null) instead of just null
}
}, 1);
});
return promise;
}
I tries using the .first(). technique described by #AT82 but I didn't find it solved the problem.
What I eventually discovered was that the form status was changing but it because I'm using onPush, the status change wasn't triggering change detection so nothing was updating in the page.
The solution I ended up going with was:
export class EmailFormComponent implements OnInit {
...
constructor(
...
private changeDetector: ChangeDetectorRef,
) {
...
// Subscribe to status changes on the form
// and use the statusChange to trigger changeDetection
this.myForm.statusChanges.pipe(
distinctUntilChanged()
).subscribe(() => this.changeDetector.markForCheck())
}
}
import { Component,
OnInit } from '#angular/core';
import { FormBuilder,
FormGroup,
FormControl,
Validators } from '#angular/forms';
import { Router } from '#angular/router';
import { EmailValidationService } from '../services/email-validation.service';
import { CustomValidators } from '../utilities/custom-validators/custom-validators';
#Component({
templateUrl: './email-form.component.html',
styleUrls: ['./email-form.component.sass']
})
export class EmailFormComponent implements OnInit {
public emailForm: FormGroup;
public formSubmitted: Boolean;
public emailSent: Boolean;
constructor(
private router: Router,
private builder: FormBuilder,
private service: EmailValidationService
) { }
ngOnInit() {
this.formSubmitted = false;
this.emailForm = this.builder.group({
email: [ '', [ Validators.required ], [ CustomValidators.briteVerifyValidator(this.service) ] ]
});
}
get email() {
return this.emailForm.get('email');
}
// rest of logic
}

Where to put service providers in angular 2 hierarchy so that components can talk to each other using the same instance of service?

Related question:
Observable do not receive the next value in angular2
No provider for service error in angular2, why do I need to inject it in it's parent component?
Using observable talk to other component in angular2, not receiving coming value
I have a PagesService that has a setCurrentPlaylists function, this function will be triggered from other component, it will receive an value of Playlists type, and will console log this value, using the next function pass to other component( I intent to).
My entire code for pages service is:
import { Injectable } from '#angular/core';
import { ApiService } from '../../apiService/api.service';
import { Platform } from '../../platforms/shared/platform.model';
import { Page } from './page.model';
import { Playlists } from '../shared/playlists.model';
import { Subject, BehaviorSubject } from 'rxjs/Rx';
#Injectable()
export class PagesService {
private currentPlaylists: Subject<Playlists> = new BehaviorSubject<Playlists>(new Playlists());
constructor(private service: ApiService) {
this.currentPlaylists.subscribe((v) => console.log(v, 'subscriber from pages service is printing out the incoming value'));
}
getPages(platform: Platform) {
return this.service.getPages(platform.value);
}
setCurrentPage(page: Page) {
this.service.setCurrentPage(page.pageId);
}
getCurrentPage():string {
return this.service.getCurrentPage();
}
getCurrentPlaylists() {
return this.currentPlaylists;
}
setCurrentPlaylists(playlists: Playlists) {
console.log("Pages Service receive an value of playlists:", playlists);
this.currentPlaylists.next(playlists);
}
}
My code for page component is:
import { Component, OnInit, Input, Output, OnChanges, EventEmitter, Injectable } from '#angular/core';
import { Http, Response } from '#angular/http';
import { Observable } from 'rxjs/Observable';
import { Platform } from '../platforms/shared/platform.model';
import { Page } from './shared/page.model';
import { Playlists } from './shared/playlists.model';
import { PagesService } from './shared/pages.service';
import { PlaylistService } from '../playlist/shared/playlist.service';
import { Subject,BehaviorSubject } from 'rxjs/Rx';
#Component({
selector: 'pages',
styleUrls: ['app/pages/pages.css'],
templateUrl: 'app/pages/pages.html',
providers: [PagesService, PlaylistService]
})
export class PagesComponent {
#Input() platform: Platform;
#Output() onPlaylistsChange: EventEmitter<Playlists>;
currentPageName: string;
currentPage: Page;
pages: Array<Page>;
playlists: Playlists;
constructor(private pageServer: PagesService, private playlistService: PlaylistService) {
this.pages = [];
this.currentPage = new Page();
this.pageServer.setCurrentPage(this.currentPage);
this.playlists = new Playlists();
this.onPlaylistsChange = new EventEmitter<Playlists>();
}
ngOnInit() {
this.pageServer.getCurrentPlaylists().subscribe((playlists) => {
console.log('subscriber in pages component is printing out the incoming value', playlists);
this.playlists = playlists;
}, error => {
console.log(error);
});
}
getPages(platform: Platform): void {
this.pageServer.getPages(platform)
.subscribe(
res => {
if (res.pages.length > 0) {
this.pages = [];
for (let page of res.pages) {
if (page.pageName !== "Shows" && page.pageName !== "All Shows" && page.pageName !== "Moives" && page.pageName !== "All Movies") {
this.pages.push(page);
}
}
this.currentPage = this.pages[0];
this.pageServer.setCurrentPage(this.currentPage);
this.currentPageName = this.pages[0].pageName;
this.getPlaylist(this.currentPage, this.platform);
} else {
this.pages = [];
this.currentPage = new Page();
this.pageServer.setCurrentPage(this.currentPage);
this.playlists = new Playlists();
this.onPlaylistsChange.emit(this.playlists);
}
},
error => console.log(error)
);
}
getPlaylist(page: Page, platform: Platform): void {
this.currentPage = page;
this.pageServer.setCurrentPage(this.currentPage);
this.playlistService.getPlaylist(page, platform)
.subscribe(
res => {
if (res.hasOwnProperty('pages') && res.pages.length > 0) {
if (res.pages[0].hasOwnProperty('bodyPlaylists') && res.pages[0].hasOwnProperty('headerPlaylists')) {
this.playlists.bodyPlaylists = res.pages[0].bodyPlaylists || [];
this.playlists.headerPlaylists = res.pages[0].headerPlaylists || [];
} else {
this.playlists.bodyPlaylists = [];
this.playlists.headerPlaylists = [];
this.playlists.wholePlaylists = res.pages[0].playlists || [];
}
this.onPlaylistsChange.emit(this.playlists);
} else {
this.playlists = new Playlists();
this.onPlaylistsChange.emit(this.playlists);
}
},
error => console.error(error)
);
}
ngOnChanges() {
// Get all Pages when the platform is set actual value;
if (this.platform.hasOwnProperty('value')) {
this.getPages(this.platform);
}
}
}
When I trigger the setCurrentPlaylists function, the playlists didn't passed to pages component. I need to use that passed value to update pages component's playlists.
This is the console output after I trigger the setCurrentPlaylsts function. No message from pages components.
Any suggestions are appreciated!
I call setCurrentPlaylists function from this component
/// <reference path="../../../typings/moment/moment.d.ts" />
import moment from 'moment';
import { Component, ViewChild, ElementRef, Input, Output, EventEmitter } from '#angular/core';
import { CORE_DIRECTIVES } from '#angular/common';
import { Http, Response } from '#angular/http';
import { MODAL_DIRECTVES, BS_VIEW_PROVIDERS } from 'ng2-bootstrap/ng2-bootstrap';
import {
FORM_DIRECTIVES,
REACTIVE_FORM_DIRECTIVES,
FormBuilder,
FormGroup,
FormControl,
Validators
} from '#angular/forms';
import { PagesService } from '../../pages/shared/pages.service';
import { ApiService } from '../../apiService/api.service';
#Component({
selector: 'assign-playlist-modal',
providers: [PagesService],
exportAs: 'assignModal',
directives: [MODAL_DIRECTVES, CORE_DIRECTIVES, FORM_DIRECTIVES, REACTIVE_FORM_DIRECTIVES ],
viewProviders: [BS_VIEW_PROVIDERS],
styleUrls: ['app/channel/shared/assignPlaylist.css'],
templateUrl: 'app/channel/modals/assignPlaylistModal.html'
})
export class AssignPlaylistModalComponent {
#ViewChild('assignPlaylistModal') modal: any;
private addPlaylistForm: FormGroup;
private playlistType: string;
private currentPage: string;
private editDate: string;
constructor(private apiService: ApiService, private pagesService: PagesService, fb: FormBuilder) {
this.currentPage = '';
this.editDate = this.apiService.getDate();
this.addPlaylistForm = fb.group({
'longPlaylistName': ['', Validators.required],
'shortPlaylistName': ['', Validators.required],
'startOn': ['', Validators.compose([
Validators.required, this.validTimeFormat
])],
'expireOn': ['', Validators.compose([
Validators.required, this.validTimeFormat
])],
'isExpire': ['']
});
this.addPlaylistForm.controls['startOn'].valueChanges.subscribe((value: string) => {
if (moment(value, 'YYYY-MM-DDThh:mm').isValid()) {
if (this.playlistType == 'dynamic') {
this.apiService.setGlobalStartTime(moment(value).format("YYYYMMDDHHmm"));
}
}
});
this.addPlaylistForm.controls['expireOn'].valueChanges.subscribe((value: string) => {
if (moment(value, 'YYYY-MM-DDThh:mm').isValid()) {
if (this.playlistType == 'dynamic') {
this.apiService.setGlobalEndTime(moment(value).format("YYYYMMDDHHmm"));
}
}
});
}
showModal(type: string) {
this.playlistType = type;
this.currentPage = this.apiService.getCurrentPage();
this.modal.show();
}
validTimeFormat(control: FormControl): { [s: string]: boolean} {
if (!moment(control.value, 'YYYY-MM-DDThh:mm').isValid()) {
return { invalidTime: true};
}
}
setCloseStyle() {
let styles = {
'color': 'white',
'opacity': 1
}
return styles;
}
createNewPlaylist(stDate: string, etDate: string, playlistTitle: string, shortTitle: string, callback?: any):any {
this.apiService.createNewPlaylist(stDate, etDate, playlistTitle, shortTitle)
.subscribe(
data => {
let playlistId = data[0].id;
this.apiService.addPlaylistToPage(playlistId, stDate, etDate, this.apiService.getGlobalRegion(), callback)
.subscribe(
data => {
if (this.apiService.g_platform == 'DESKTOP') {
this.apiService.getPlaylist(this.apiService.getCurrentPage(), 'true' )
.subscribe(
res => {
if (res.hasOwnProperty('pages') && res.pages.length > 0) {
if (res.pages[0].hasOwnProperty('bodyPlaylists') && res.pages[0].hasOwnProperty('headerPlaylists')) {
this.apiService.getCurrentPlaylists().bodyPlaylists = res.pages[0].bodyPlaylists || [];
this.apiService.getCurrentPlaylists().headerPlaylists = res.pages[0].headerPlaylists || [];
console.log('assign playlist component is calling the pages service setCurrentPlaylists function.');
this.pagesService.setCurrentPlaylists(this.apiService.getCurrentPlaylists());
} else {
this.apiService.getCurrentPlaylists().bodyPlaylists = [];
this.apiService.getCurrentPlaylists().headerPlaylists = [];
this.apiService.getCurrentPlaylists().wholePlaylists = res.pages[0].playlists || [];
console.log('assign playlist component is calling the pages service setCurrentPlaylists function.');
this.pagesService.setCurrentPlaylists(this.apiService.getCurrentPlaylists());
}
}
}
);
} else {
this.apiService.getPlaylist(this.apiService.getCurrentPage(), 'false' )
.subscribe(
res => {
if (res.hasOwnProperty('pages') && res.pages.length > 0) {
this.apiService.getCurrentPlaylists().bodyPlaylists = [];
this.apiService.getCurrentPlaylists().headerPlaylists = [];
this.apiService.getCurrentPlaylists().wholePlaylists = res.pages[0].playlists || [];
console.log('assign playlist component is calling the pages service setCurrentPlaylists function.');
this.pagesService.setCurrentPlaylists(this.apiService.getCurrentPlaylists());
}
}
);
}
}
);
},
error => console.log(error)
);
}
onSubmit(form: FormGroup) {
// get start time, the format from input will be like 2016-06-07T00:05
let startTime = moment(form.value.startOn).format("YYYYMMDDHHmm");
let expireTime = moment(form.value.expireOn).format("YYYYMMDDHHmm");
let playlistTitle = form.value.longPlaylistName;
let shortTitle = form.value.shortPlaylistName;
if (this.playlistType == 'smart' || this.playlistType == 'new') {
this.createNewPlaylist(startTime, expireTime, playlistTitle, shortTitle);
}
}
}
This is my component tree:
I am assuming your components tree is as follow:
AssignPlaylistModalComponent (Parent or higher level than PagesComponent in the tree)
PagesComponent (lowest level child as it does not import any directive)
Issue
You should only put your service in the top level (parent) components provider. Though all components still need to do the import and constructor.
Putting the service in a component's provider will create a new copy of the service and share along the component tree downward, not upward.
In the code in question, PagesComponent, as the lowest level child in the tree, with its own provider line, is actually initiating its own copy of PagesService, PlaylistService. So each instance of PagesComponent is basically listening to itself only. It won't receive any messages from others.
Fix
#Component({
selector: 'pages',
styleUrls: ['app/pages/pages.css'],
templateUrl: 'app/pages/pages.html',
providers: [PagesService, PlaylistService] // <--- Delete this line
})
export class PagesComponent {
#Input() platform: Platform;
#Output() onPlaylistsChange: EventEmitter<Playlists>;
Where to put providers
Assume following component tree:
Component A1 (root component)
Component B1
Component C1
Component C2
Component B2
Component C3
Component C4
The easiest way is to put it in A1 providers, all components will be sharing the same service instance, and able to message each other.
If you put it in B1 providers, then only B1, C1 and C2 can talk to each other.
Base on lastest update, the root component of the project is AppComponent.ts. providers should be added in it.
From the code you provided, I cannot see when this method
setCurrentPlaylists(playlists: Playlists) {
console.log(playlists, 'i am here');
this.currentPlaylists.next(playlists);
}
is called. Therefore, your list is empty.
Doing this
this.pageServer.getCurrentPlaylists().subscribe((playlists) => {
console.log(playlists, 'new playlists coming');
this.playlists = playlists;
}, error => {
console.log(error);
});
only creates a subscription to the observable. You need to publish data from somewhere.
In addition, it'd better to move this code
this.pageServer.getCurrentPlaylists().subscribe((playlists) => {
console.log(playlists, 'new playlists coming');
this.playlists = playlists;
}, error => {
console.log(error);
});
to ngOnInit()

Categories

Resources