this is my question:
i have a custom component which has a template with a ngModel inside.
import { Component, Input, forwardRef } from '#angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '#angular/forms';
const listaMesi = [
{
value: '01',
text: 'Gennaio'
}, {
value: '02',
text: 'Febbraio'
}, {
value: '03',
text: 'Marzo'
}, {
value: '04',
text: 'Aprile'
}, {
value: '05',
text: 'Maggio'
}, {
value: '06',
text: 'Giugno'
}, {
value: '07',
text: 'Luglio'
}, {
value: '08',
text: 'Agosto'
}, {
value: '09',
text: 'Settembre'
}, {
value: '10',
text: 'Ottobre'
}, {
value: '11',
text: 'Novembre'
}, {
value: '12',
text: 'Dicembre'
}
]
const annoCorrente = new Date().getFullYear();
#Component({
selector: 'seg-month-picker',
templateUrl: './month-picker.component.html',
styleUrls: ['./month-picker.component.scss'],
providers: [{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => MonthPickerComponent),
multi: true
}]
})
export class MonthPickerComponent implements ControlValueAccessor {
private propagateChange: Function;
private checkedValue: string;
private isDisabled = true;
public meseSelezionato: string;
public annoSelezionato: string;
public yearList = [];
public monthList = listaMesi;
private _min: string;
private _max: string;
#Input() set min(value: string) {
this._min = value;
if (value) {
const [ year, month ] = value.split('-');
const maxYear = this._max ? +this._max.slice(0, 4) : annoCorrente;
this.yearList = Array.from({ length: maxYear + 1 - +year }).map((_, index) => +year + index);
}
}
get min() {
return this._min;
}
#Input() set max(value: string) {
this._max = value;
if (value) {
const [ maxYear, month ] = value.split('-');
const year = this._min ? +this.min.slice(0, 4) : annoCorrente;
this.yearList = Array.from({length: +maxYear - year + 1}).map((_, index) => year + index);
}
}
get max() {
return this._max;
}
updateYear(year: string) {
this.annoSelezionato = year;
this.updateValue();
}
updateMonth(month: string) {
this.meseSelezionato = month;
this.updateValue();
}
updateValue() {
if (this.annoSelezionato && this.meseSelezionato && this.propagateChange) {
this.propagateChange(`${this.annoSelezionato}-${this.meseSelezionato}`);
}
}
writeValue(yearMonth: string): void {
const [ year, month ] = yearMonth.split('-');
this.annoSelezionato = year;
this.meseSelezionato = month;
}
registerOnChange(fn: Function): void {
this.propagateChange = fn;
}
registerOnTouched(fn: Function): void { }
setDisabledState(isDisabled: boolean): void {
this.isDisabled = isDisabled;
}
}
<div>
<select id="anno" name="anno" [ngModel]="annoSelezionato" (ngModelChange)="updateYear($event)">
<option *ngFor="let anno of yearList" [value]="anno.value">{{anno.value}}</option>
</select>
<select id="mese" name="mese" [ngModel]="meseSelezionato" (ngModelChange)="updateMonth($event)">
<option *ngFor="let mese of monthList" [value]="mese.value">{{mese.text}}</option>
</select>
</div>
The problem is: how i track ngModule changing in my unit test? i'll paste my unit test which is not working; i tried with #viewChild() but i surely got something wrong.
import { Component, ViewChild } from '#angular/core';
import { async, ComponentFixture, TestBed } from '#angular/core/testing';
import { FormsModule } from '#angular/forms';
import { MonthPickerComponent } from './month-picker.component';
#Component({
selector: 'seg-month-picker',
template: `<seg-month-picker>`
})
export class TestComponent {
#ViewChild(MonthPickerComponent) picker;
}
function getComponent(): Promise<TestComponent> {
const fixture = TestBed
.createComponent(TestComponent);
fixture.detectChanges();
return fixture.whenStable().then(() => fixture.componentInstance);
}
describe('MonthPickerComponent', async() => {
let component: MonthPickerComponent;
let fixture: ComponentFixture<MonthPickerComponent>;
let element: HTMLElement;
const mockedComponent = await getComponent();
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [MonthPickerComponent, mockedComponent],
imports: [FormsModule]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(MonthPickerComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should be created', () => {
expect(component).toBeTruthy();
});
describe('setMin', () => {
it('deve impostare la lista di anni in base al minimo dato', () => {
component.min = '2014-03';
fixture.detectChanges();
expect(component.yearList).toEqual([2014, 2015, 2016, 2017]);
})
});
describe('getMin', () => {
it('deve restituire l\'anno minimo della lista', () => {
component.min = '2014-03';
fixture.detectChanges();
const result = component.min;
expect(result).toBe('2014-03');
});
it('deve restituire null se non ho un valore minimo', () => {
component.min = undefined;
fixture.detectChanges();
const result = component.min;
expect(result).toBe(undefined);
})
});
describe('setMax', () => {
it('deve restituire la lista di anni in base al massimo dato', () => {
component.max = '2018-01';
fixture.detectChanges();
expect(component.yearList).toEqual([2017, 2018]);
});
it('deve restituire la lista di anni in base al range dato', () => {
component.max = '2018-01';
component.min = '2014-01';
fixture.detectChanges();
expect(component.yearList).toEqual([2014, 2015, 2016, 2017, 2018]);
});
});
describe('getMax', () => {
it('deve restituire l\'anno massimo della lista', () => {
component.max = '2018-01';
fixture.detectChanges();
const result = component.max;
expect(result).toBe('2018-01');
});
it('deve restituire null se non ho un valore massimo', () => {
component.max = undefined;
fixture.detectChanges();
const result = component.max;
expect(result).toBe(undefined);
});
});
describe('writeValue', () => {
fit('deve modificare il valore all\'ngModel del componente', async () => {
console.log(mockedComponent.picker);
mockedComponent.picker.writeValue('2016-03');
fixture.detectChanges();
const result = component.max;
expect(result).toBe('2018-01');
});
it('deve restituire null se non ho un valore massimo', () => {
component.max = undefined;
fixture.detectChanges();
const result = component.max;
expect(result).toBe(undefined);
});
});
});
The problem is that i have to test the writeValue() function and the others from the component controller, tracking which values the ngModel assumes. i don't really know how to resolve this problem.
Thanks for the help guys!
--- EDIT ---
At least i could take an html reference to the <select> in my component view and change its value 'programmatically', but i think there's a better approach.
try this:
import { Component, Input, forwardRef } from '#angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '#angular/forms';
const listaMesi = [
{
value: '01',
text: 'Gennaio'
}, {
value: '02',
text: 'Febbraio'
}, {
value: '03',
text: 'Marzo'
}, {
value: '04',
text: 'Aprile'
}, {
value: '05',
text: 'Maggio'
}, {
value: '06',
text: 'Giugno'
}, {
value: '07',
text: 'Luglio'
}, {
value: '08',
text: 'Agosto'
}, {
value: '09',
text: 'Settembre'
}, {
value: '10',
text: 'Ottobre'
}, {
value: '11',
text: 'Novembre'
}, {
value: '12',
text: 'Dicembre'
}
]
const annoCorrente = new Date().getFullYear();
#Component({
selector: 'seg-month-picker',
templateUrl: './month-picker.component.html',
styleUrls: ['./month-picker.component.scss'],
providers: [{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => MonthPickerComponent),
multi: true
}]
})
export class MonthPickerComponent implements ControlValueAccessor {
private propagateChange: Function;
private checkedValue: string;
private isDisabled = true;
public meseSelezionato: string;
public annoSelezionato: string;
public yearList = [];
public monthList = listaMesi;
private _min: string;
private _max: string;
#Input() set min(value: string) {
this._min = value;
if (value) {
const [ year, month ] = value.split('-');
const maxYear = this._max ? +this._max.slice(0, 4) : annoCorrente;
this.yearList = Array.from({ length: maxYear + 1 - +year }).map((_, index) => +year + index);
}
}
get min() {
return this._min;
}
#Input() set max(value: string) {
this._max = value;
if (value) {
const [ maxYear, month ] = value.split('-');
const year = this._min ? +this.min.slice(0, 4) : annoCorrente;
this.yearList = Array.from({length: +maxYear - year + 1}).map((_, index) => year + index);
}
}
get max() {
return this._max;
}
updateYear(year: string) {
this.annoSelezionato = year;
this.updateValue();
}
updateMonth(month: string) {
this.meseSelezionato = month;
this.updateValue();
}
updateValue() {
if (this.annoSelezionato && this.meseSelezionato && this.propagateChange) {
this.propagateChange(`${this.annoSelezionato}-${this.meseSelezionato}`);
}
}
writeValue(yearMonth: string): void {
const [ year, month ] = yearMonth.split('-');
this.annoSelezionato = year;
this.meseSelezionato = month;
}
registerOnChange(fn: Function): void {
this.propagateChange = fn;
}
registerOnTouched(fn: Function): void { }
setDisabledState(isDisabled: boolean): void {
this.isDisabled = isDisabled;
}
}
Related
I want to access value present in JSON object with configurable targetPath for response received.
Below is my component class which will bind the data present in list to the autocomplete drop down in HTML code
the configurable key is 'path', which value I want to provide as a key in custom JSON object key 'field'.
import { Component, ViewChild } from '#angular/core';
import { MenuItem } from 'primeng/api';
import { SelectItem } from 'primeng/api';
import { SelectItemGroup } from 'primeng/api';
import { FilterService } from 'primeng/api';
import { AutoComplete } from 'primeng/autocomplete';
import { CountryService } from './countryservice';
#Component({
selector: 'app-root',
templateUrl: './app.component.html',
providers: [CountryService, FilterService]
})
export class AppComponent {
userDetails: any[];
selectedUserDetails: any[];
selectedValue: any;
selectedUserDetail: any;
path: string = 'contactMedium[0].characteristic.emailAddress';
testVal: string;
constructor() {}
ngOnInit() {
this.userDetails = [
{
id: 'cont-609',
contactMedium: [
{
characteristic: {
emailAddress: 'test#gmail.com'
}
}
]
},
{
id: 'cont-610',
contactMedium: [
{
characteristic: {
emailAddress: 'test#gmail.com'
}
}
]
},
{
id: 'cont-611',
contactMedium: [
{
characteristic: {
emailAddress: 'test#gmail.com'
}
}
]
},
{
id: 'cont-612',
contactMedium: [
{
characteristic: {
emailAddress: 'test#gmail.com'
}
}
]
},
{
id: 'cont-614',
contactMedium: [
{
characteristic: {
emailAddress: 'test#gmail.com'
}
}
]
}
];
}
filterUserDetails(event) {
const filteredNew: any[] = [];
this.getUserDetails().then(response => {
for (let resp of response) {
this.testVal = resp[this.path];
filteredNew.push({
id: resp.id,
field: resp[this.path]
});
}
});
this.selectedUserDetails = filteredNew;
}
getUserDetails(): Promise<any[]> {
return Promise.resolve(this.userDetails);
}
chooseItem(event) {
this.selectedUserDetail =
event.contactMedium[0].characteristic.emailAddress;
}
}
From the response received from method getUserDetails(), I am building a custom JSON object array with fields 'id' and 'field', the key for id is known which itself is a 'id' but key for field is configurable which is path in my case.
But looks like above logic where key is accessed is not working as expected i.e I am not getting value for
filterUserDetails(event) {
const filteredNew: any[] = [];
this.getUserDetails().then(response => {
for (let resp of response) {
this.testVal = resp[this.path];
filteredNew.push({
id: resp.id,
field: resp[this.path]
});
}
});
this.selectedUserDetails = filteredNew;
}
Below is my HTML code
<h5>Dropdown Testing</h5>
<p>selectedUserDetail : {{selectedUserDetail}}</p>
<p>TestVal : {{testVal}}</p>
<p-autoComplete [(ngModel)]="selectedUserDetail" [suggestions]="selectedUserDetails"
(completeMethod)="filterUserDetails($event)" [dropdown]="true" field="field">
<ng-template let-userDetails pTemplate=" item">
<div>{{userDetails.field}}</div>
</ng-template>
</p-autoComplete>
If I change the usage of assignment like below everything works fine
field: resp.contactMedium[0].characteristic.emailAddress
Link of my code is here : https://stackblitz.com/edit/primeng-autocomplete-demo-dyihrs?file=src%2Fapp%2Fapp.component.html
Expectation here is to assign value of key: contactMedium[0].characteristic.emailAddress received from getUserDetails() to custom JSON object which is getting build in filterUserDetails() to 'field' key.
You should be aware that your field can be very complex but in the case you mentioned, can be resolved with this:
resolveField(data: any, field: string): any {
if (data && field) {
let fields: string[] = field.split('.');
let value = data;
for(let i = 0, len = fields.length; i < len; ++i) {
if (value == null) {
return null;
} else {
const pos: number = fields[i].match(/\d+/g)?.[0];
if(pos != null) {
const property = fields[i].match(/^\w+/g);
value = value[property][pos];
} else {
value = value[fields[i]];
}
}
}
return value;
} else {
return null;
}
}
You can use the resolveField function as part of your logic. You can modify it, as you want or required, for example: here is considering only letters as part of the property names.
Here is the solution for your code.
You can use a variable that has the dynamic key
var obj = {
...
}
var key = ...;
obj[key] = somevalue;
or to get the value
var somevalue = obj[key];
i want to show items in Tree Items in angular 9 and angular material .
my problem is here :
i want to show 3 or 4 level in tree . i send a request to server and get flat item , and i need to convert that to multi level model .
intialData(): any {
this.searchParam.page = 1;
this.searchParam.rows = 1000;
this.claimsManagerService.getAll(this.searchParam).subscribe(data => {
data['records'].forEach(element => {
let model = {} as FileNode;
if (element.parentId == null) {
model.id = element.id;
model.isChilde = element.isChilde;
model.parentId = element.parentId;
model.title = element.title;
data['records'].forEach(child => {
if (child.parentId == element.id) {
let childe = {} as FileNode;
childe.id = child.id;
childe.isChilde = child.isChilde;
childe.parentId = child.parentId;
childe.title = child.title;
if (!model.children) model.children = [] as FileNode[];
model.children.push(childe)
data['records'].forEach(childs => {
if (childs.parentId === child.id) {
let childe = {} as TreeNode;
childe.id = childs.id;
childe.isChilde = childs.isChilde;
childe.parentId = child.parentId;
childe.title = child.title;
let item = model.children.find(x => x.id == childs.parentId);
if (!item.children) item.children = [] as TreeNode[];
item.children.push(childe);
}
})
}
})
this.dataSource.data.push(model)
}
})
return this.dataSource.data
})
}
i write this code for show data in tree :
export interface FileNode {
id: number;
title: string;
parentId: number;
isChilde: boolean;
children?: FileNode[];
}
export interface TreeNode {
id: number;
title: string;
parentId: number;
isChilde: boolean;
level: number;
expandable: boolean;
}
#Component({
selector: 'kt-role-tree',
templateUrl: './tree.component.html',
styleUrls: ['./tree.component.css'],
providers: [TreeBuilder]
})
export class TreeComponent implements OnInit,AfterViewInit {
#Output() selectedList = new EventEmitter<any>();
/** The TreeControl controls the expand/collapse state of tree nodes. */
treeControl: FlatTreeControl<TreeNode>;
treeFlattener: MatTreeFlattener<FileNode, TreeNode>;
searchParam: TableSearch;
dataSource: MatTreeFlatDataSource<FileNode, TreeNode>;
treeModel :object;
constructor(private database: TreeBuilder,
private claimsManagerService: ClaimsManagerService,
private cdRef: ChangeDetectorRef,
private dialog: MatDialog) {
this.treeFlattener = new MatTreeFlattener(
this.transformer,
this.getLevel,
this.isExpandable,
this.getChildren);
this.searchParam = {
_search: true,
dateTimeType: 1,
page: 1,
rows: 2
};
this.treeControl = new FlatTreeControl<TreeNode>(this.getLevel, this.isExpandable);
this.dataSource = new MatTreeFlatDataSource(this.treeControl, this.treeFlattener);
this.intialData();
}
ngAfterViewInit(): void {
this.cdRef.detectChanges();
}
ngOnInit(): void {
}
/** Transform the data to something the tree can read. */
transformer(node: FileNode, level: number) {
return {
id: node.id,
title: node.title,
parentId: node.parentId,
isChilde: node.isChilde,
level: level,
expandable: !!node.children
};
}
/** Get the level of the node */
getLevel(node: TreeNode) {
return node.level;
}
/** Return whether the node is expanded or not. */
isExpandable(node: TreeNode) {
return node.expandable;
};
/** Get the children for the node. */
getChildren(node: FileNode) {
return observableOf(node.children);
}
/** Get whether the node has children or not. */
hasChild(node: TreeNode) {
return node.expandable;
}
delete(id: number): void {
const title = 'Post Delete';
const itemName = `Post `;
const service = this.claimsManagerService;
const dialogRef = this.dialog.open(DeleteEntityDialogComponent, {
data: { id, title, itemName, service },
width: '440px'
});
dialogRef.afterClosed().subscribe(res => {
if (res) {
this.intialData();
}
});
}
openAdd(id, title): void {
let dialogRef;
if (typeof (id) === 'string') {
dialogRef = this.dialog.open(ClaimsManagerAddComponent, {
data: { id: null, isChilde: false, claimName: 'Main' }
});
} else {
dialogRef = this.dialog.open(ClaimsManagerAddComponent, {
data: { id: id, isChilde: true, claimName: title }
});
}
dialogRef.afterClosed().subscribe(() => {
this.intialData();
})
}
intialData(): any {
this.searchParam.page = 1;
this.searchParam.rows = 1000;
this.claimsManagerService.getAll(this.searchParam).subscribe(data => {
data['records'].forEach(element => {
let model = {} as FileNode;
if (element.parentId == null) {
model.id = element.id;
model.isChilde = element.isChilde;
model.parentId = element.parentId;
model.title = element.title;
data['records'].forEach(child => {
if (child.parentId == element.id) {
let childe = {} as FileNode;
childe.id = child.id;
childe.isChilde = child.isChilde;
childe.parentId = child.parentId;
childe.title = child.title;
if (!model.children) model.children = [] as FileNode[];
model.children.push(childe)
data['records'].forEach(childs => {
if (childs.parentId === child.id) {
let childe = {} as TreeNode;
childe.id = childs.id;
childe.isChilde = childs.isChilde;
childe.parentId = child.parentId;
childe.title = child.title;
let item = model.children.find(x => x.id == childs.parentId);
if (!item.children) item.children = [] as TreeNode[];
item.children.push(childe);
}
})
}
})
this.dataSource.data.push(model)
}
})
return this.dataSource.data
})
}
}
and this is the html code :
<mat-tree [dataSource]="dataSource" [treeControl]="treeControl">
<mat-tree-node *matTreeNodeDef="let node" matTreeNodeToggle matTreeNodePadding>
<button mat-icon-button disabled></button>
{{node.title}}
</mat-tree-node>
<mat-tree-node *matTreeNodeDef="let node;when: hasChild" matTreeNodePadding>
<button mat-icon-button matTreeNodeToggle
[attr.aria-label]="'toggle ' + node.title">
<mat-icon class="mat-icon-rtl-mirror">
{{treeControl.isExpanded(node) ? 'expand_more' : 'chevron_right'}}
</mat-icon>
</button>
{{node.title}} {{node.id}}
</mat-tree-node>
but it not show me any things in html .
whats the problem ? how can i solve this problem ????
I'm trying to make a test using jest with Vue.
the details below.
Problem:
Can't mount using shallowMount option.
Situation:
Run the test after mounting the component using shallowMount option that provides in Vue-test-utils.
Throw error "Cannot read property 'XXXX' of undefined
This is my test code.
import myComponent from '#/~';
import Vuex from 'vuex';
import Vuelidate from 'vuelidate';
import { shallowMount, createLocalVue } from '#vue/test-utils';
const localVue = createLocalVue();
localVue.use(Vuex);
localVue.use(Vuelidate);
describe('myComponent~', () => {
let store;
beforeEach(() => {
store = new Vuex.Store({
modules: {
user: {
namespaced: true,
getters: {
profile: () => {
const profile = { name: 'blahblah' };
return profile;
},
},
},
},
});
});
describe('profile.name is "blahblah"', () => {
it('return something~', () => {
const wrapper = shallowMount(myComponent, {
localVue,
store,
mocks: {
$api: {
options: {
testMethod() {
return new Promise((resolve, reject) => {
resolve('test');
});
},
},
},
$i18n: {
t() {
return {
EN: 'EN',
KO: 'KO',
JP: 'JA',
SC: 'zh-CN',
TC: 'tw-CN',
};
},
},
},
});
expect(wrapper.find('.profile').text()).toBe('blahblah');
});
I think the problem is that property isn't set as a specified value or an empty value like an array or object.
But I don't know how I set properly the properties in my logic.
For example,
when the error yields "Cannot read property 'images' of undefined",
I add to a wrapper in the relevant method like this.
exampleMethod() {
this.something = this.something.map(item => {
if (item.detailContent.images) { // <-- the added wrapper is here
~~~logic~~~~
}
})
}
But the undefined properties are so many, I also think this way is not proper.
How I do solve this problem?
added
These are details about the above example method:
exampleMethod() {
this.something = this.something.map(item => {
let passValidation = false;
let failValidation = false;
if (item.detailContent.images) {
if (this.detail.showLanguages.includes(item.code)) {
if (this.configId !== 'OPTION1') {
item.detailContent.images = item.detailContent.images.map(element => {
return {
...element,
required: true,
}
});
}
checkValidationPass = true;
} else {
if (this.configId !== 'OPTION1') {
item.detailContent.images = item.detailContent.images.map(element => {
return {
...element,
required: false,
}
});
}
checkValidationPass = false;
}
return {
...item,
required: passValidation,
warning: failValidation,
}
}
});
if (this.configId === 'OPTION2') {
this.checkOption2Validation();
} else if (this.configId === 'OPTION3') {
this.checkOption3Validation();
} else {
this.checkOption1Validation();
}
},
And this is 'this.something':
data() {
return {
something: []
}
}
The detailContent is set here.
setMethod() {
this.something = [
...this.otherthings,
];
this.something = this.something.map(item => {
let details1 = {};
if (this.configId === 'OPTION2') {
details1 = {
images: [
{ deviceType: 'PC', titleList: [null, null], imageType: 'IMAGE' },
{ deviceType: 'MOBILE', titleList: [null, null, null] }
]
};
} else if (this.configId === 'OPTION3') {
details1 = {
images: [
{ deviceType: 'PC' },
{ deviceType: 'MOBILE' }
],
links: { linkType: 'EMPTY' },
};
}
let details2 = {
mainTitle: {
content: null,
}
}
let checkValidation = false;
this.detail.detailLanguages.forEach(element => {
if (element.language === item.code) {
details1 = { ...element };
if (!!element.mainTitle) {
details2 = { ...element };
} else {
details2 = {
...details2,
...element
};
}
if (this.configId !== 'OPTION1') {
details1.images = details1.images.map(image => {
return {
...image,
required: true,
}
});
}
checkValidation = true;
}
});
return {
...item,
detailContent: this.configId !== 'OPTION1' ? details1 : details2,
required: false,
warning: false,
}
});
},
I've come across an issue that I'm having difficulty solving. There are two issues both of which appear to be caused by race conditions.
1.) The drawPoll() function is getting executed before the this.poll.choices are done appending. I confirmed that this is the issue by manually adding a 3 second setTimeout(). How do I make sure the drawPoll() function only executes after the choices.ForEach() iteration is complete?
2.) When calling the vote(choiceId) function and decrementing a choice the firebaseService observable does not pick up the correct value for "votes" because the observable kicks in before the vote deletion has finished executing. How do I re-arrange my code so that the observable waits until the vote document deletion is complete?
I tried to wrap the choices.forEach iteration in a promise but had difficulty getting this to work. And I wasn't sure where to even start to do a promise chain for the decrementChoice() and getChoices() since the getChoices() function doesn't always rely on the decrementChoice or incrementChoice() function when it's initilaized. It only relies on those when a vote is casted. Attached is my component and firebase service. Any help would be greatly appreciated!
poll.component.ts
import { Component, OnInit } from '#angular/core';
import * as Chart from 'chart.js';
import { Observable } from 'rxjs';
import { FirebaseService } from '../services/firebase.service';
import { first } from 'rxjs/operators';
import { Input, Output, EventEmitter } from '#angular/core';
import { CardModule } from 'primeng/card';
#Component({
selector: 'app-poll',
templateUrl: './poll.component.html',
styleUrls: ['./poll.component.scss']
})
export class PollComponent implements OnInit {
chart:any;
poll:any;
votes:[] = [];
labels:string[] = [];
title:string = "";
isDrawn:boolean = false;
inputChoices:any = [];
username:string = "";
points:number;
#Input()
pollKey: string;
#Output()
editEvent = new EventEmitter<string>();
#Output()
deleteEvent = new EventEmitter<string>();
constructor(private firebaseService: FirebaseService) { }
ngOnInit() {
this.firebaseService.getPoll(this.pollKey).subscribe(pollDoc => {
// ToDo: draw poll choices on create without breaking vote listener
console.log("details?", pollDoc);
// Return if subscription was triggered due to poll deletion
if (!pollDoc.payload.exists) {
return;
}
const pollData:any = pollDoc.payload.data();
this.poll = {
id: pollDoc.payload.id,
helperText: pollData.helperText,
pollType: pollData.pollType,
scoringType: pollData.scoringType,
user: pollData.user
};
if (this.poll.pollType == 1) {
this.title = "Who Do I Start?";
}
if (this.poll.pollType == 2) {
this.title = "Who Do I Drop?";
}
if (this.poll.pollType == 3) {
this.title = "Who Do I Pick Up?";
}
if (this.poll.pollType == 4) {
this.title = "Who Wins This Trade?";
}
// Populate username and user points
this.firebaseService.getUser(pollData.user).subscribe((user:any) => {
const userDetails = user.payload._document.proto;
if (userDetails) {
this.username = userDetails.fields.username.stringValue;
this.points = userDetails.fields.points.integerValue;
}
});
this.firebaseService.getChoices(this.pollKey).pipe(first()).subscribe(choices => {
console.log("get choices");
this.poll.choices = [];
choices.forEach(choice => {
const choiceData:any = choice.payload.doc.data();
const choiceKey:any = choice.payload.doc.id;
this.firebaseService.getVotes(choiceKey).pipe(first()).subscribe((votes: any) => {
this.poll.choices.push({
id: choiceKey,
text: choiceData.text,
votes: votes.length
});
});
this.firebaseService.getVotes(choiceKey).subscribe((votes: any) => {
if (this.isDrawn) {
const selectedChoice = this.poll.choices.find((choice) => {
return choice.id == choiceKey
});
selectedChoice.votes = votes.length;
this.drawPoll();
}
});
});
setTimeout(() => {
this.drawPoll();
}, 3000)
});
});
}
drawPoll() {
if (this.isDrawn) {
this.chart.data.datasets[0].data = this.poll.choices.map(choice => choice.votes);
this.chart.data.datasets[0].label = this.poll.choices.map(choice => choice.text);
this.chart.update()
}
if (!this.isDrawn) {
console.log("text?", this.poll.choices.map(choice => choice.text));
this.inputChoices = this.poll.choices;
var canvas = <HTMLCanvasElement> document.getElementById(this.pollKey);
var ctx = canvas.getContext("2d");
this.chart = new Chart(ctx, {
type: 'horizontalBar',
data: {
labels: this.poll.choices.map(choice => choice.text),
datasets: [{
label: this.title,
data: this.poll.choices.map(choice => choice.votes),
fill: false,
backgroundColor: [
"rgba(255, 4, 40, 0.2)",
"rgba(19, 32, 98, 0.2)",
"rgba(255, 4, 40, 0.2)",
"rgba(19, 32, 98, 0.2)",
"rgba(255, 4, 40, 0.2)",
"rgba(19, 32, 98, 0.2)"
],
borderColor: [
"rgb(255, 4, 40)",
"rgb(19, 32, 98)",
"rgb(255, 4, 40)",
"rgb(19, 32, 98)",
"rgb(255, 4, 40)",
"rgb(19, 32, 98)",
],
borderWidth: 1
}]
},
options: {
events: ["touchend", "click", "mouseout"],
onClick: function(e) {
console.log("clicked!", e);
},
tooltips: {
enabled: true
},
title: {
display: true,
text: this.title,
fontSize: 14,
fontColor: '#666'
},
legend: {
display: false
},
maintainAspectRatio: true,
responsive: true,
scales: {
xAxes: [{
ticks: {
beginAtZero: true,
precision: 0
}
}]
}
}
});
this.isDrawn = true;
}
}
vote(choiceId) {
if (choiceId) {
const choiceInput:any = document.getElementById(choiceId);
const checked = choiceInput.checked;
this.poll.choices.forEach(choice => {
const choiceEl:any = document.getElementById(choice.id);
if (choiceId !== choiceEl.id && checked) choiceEl.disabled = true;
if (!checked) choiceEl.disabled = false;
});
if (checked) this.firebaseService.incrementChoice(choiceId);
if (!checked) this.firebaseService.decrementChoice(choiceId);
}
}
edit() {
this.editEvent.emit(this.poll);
}
delete() {
this.deleteEvent.emit(this.poll);
}
}
firebase.service.ts
import { Injectable } from '#angular/core';
import { AngularFirestore } from '#angular/fire/firestore';
import { map, switchMap, first } from 'rxjs/operators';
import { Observable, from } from 'rxjs';
import * as firebase from 'firebase';
import { AngularFireAuth } from '#angular/fire/auth';
#Injectable({
providedIn: 'root'
})
export class FirebaseService {
// Source: https://github.com/AngularTemplates/angular-firebase-crud/blob/master/src/app/services/firebase.service.ts
constructor(public db: AngularFirestore, private afAuth: AngularFireAuth) { }
getPoll(pollKey) {
return this.db.collection('polls').doc(pollKey).snapshotChanges();
}
getChoices(pollKey) {
return this.db.collection('choices', ref => ref.where('poll', '==', pollKey)).snapshotChanges();
}
incrementChoice(choiceKey) {
const userId = this.afAuth.auth.currentUser.uid;
const choiceDoc:any = this.db.collection('choices').doc(choiceKey);
// Check if user voted already
choiceDoc.ref.get().then(choice => {
let pollKey = choice.data().poll
this.db.collection('votes').snapshotChanges().pipe(first()).subscribe((votes:any) => {
let filteredVote = votes.filter((vote) => {
const searchedPollKey = vote.payload.doc._document.proto.fields.poll.stringValue;
const searchedChoiceKey = vote.payload.doc._document.proto.fields.choice.stringValue;
const searchedUserKey = vote.payload.doc._document.proto.fields.user.stringValue;
return (searchedPollKey == pollKey && searchedChoiceKey == choiceKey && searchedUserKey == userId);
});
if (filteredVote.length) {
// This person aleady voted
return false;
} else {
let votes = choice.data().votes
choiceDoc.update({
votes: ++votes
});
const userDoc:any = this.db.collection('users').doc(userId);
userDoc.ref.get().then(user => {
let points = user.data().points
userDoc.update({
points: ++points
});
});
this.createVote({
choiceKey: choiceKey,
pollKey: pollKey,
userKey: userId
});
}
});
});
}
decrementChoice(choiceKey) {
const choiceDoc:any = this.db.collection('choices').doc(choiceKey);
const userId = this.afAuth.auth.currentUser.uid;
choiceDoc.ref.get().then(choice => {
let pollKey = choice.data().poll
let votes = choice.data().votes
choiceDoc.update({
votes: --votes
});
const userDoc:any = this.db.collection('users').doc(userId);
userDoc.ref.get().then(user => {
let points = user.data().points
userDoc.update({
points: --points
});
});
// Find & delete vote
this.db.collection('votes').snapshotChanges().pipe(first()).subscribe((votes:any) => {
let filteredVote = votes.filter((vote) => {
const searchedPollKey = vote.payload.doc._document.proto.fields.poll.stringValue;
const searchedChoiceKey = vote.payload.doc._document.proto.fields.choice.stringValue;
const searchedUserKey = vote.payload.doc._document.proto.fields.user.stringValue;
return (searchedPollKey == pollKey && searchedChoiceKey == choiceKey && searchedUserKey == userId);
});
this.deleteVote(filteredVote[0].payload.doc.id);
});
});
}
createVote(value) {
this.db.collection('votes').add({
choice: value.choiceKey,
poll: value.pollKey,
user: value.userKey
}).then(vote => {
console.log("Vote created successfully", vote);
}).catch(err => {
console.log("Error creating vote", err);
});
}
deleteVote(voteKey) {
this.db.collection('votes').doc(voteKey).delete().then((vote) => {
console.log("Vote deleted successfully");
}).catch(err => {
console.log("Error deleting vote", err);
});
}
getVotes(choiceKey) {
return this.db.collection('votes', ref => ref.where('choice', '==', choiceKey)).snapshotChanges().pipe(first());
}
}
** UPDATE **
I was able to solve issue #2 by creating a separate subcription just for vote updates. The code feels rather cumberson but at-least now #2 is no longer an issue. I'm still encountering the same issue though with #1 where the drawPoll() function is getting executed before this.poll.choices are done iterating and appending. I updated the question to reflect my updated code.
I am trying to set up a mutation with modified code from the Relay Todo example.
When I try to compile I get the following error:
-- GraphQL Validation Error -- AddCampaignMutation --
File: /Users/me/docker/relay/examples/todo/js/mutations/AddCampaignMutation.js
Error: Cannot query field "addCampaign" on type "Mutation".
Source:
>
> mutation AddCampaignMutation {addCampaign}
> ^^^
-- GraphQL Validation Error -- AddCampaignMutation --
File: /Users/me/docker/relay/examples/todo/js/mutations/AddCampaignMutation.js
Error: Unknown type "AddCampaignPayload". Did you mean "AddTodoPayload" or "RenameTodoPayload"?
Source:
>
> fragment AddCampaignMutationRelayQL on AddCampaignPayload #relay(pattern: true) {
> ^^
I have duplicated the Todo code so I don't know why the Todo mutation is working correctly but my new Campaign test isn't.
This is my database.js file, I have removed the Todo related items to make the document easier to read:
export class Campaign {}
export class User {}
// Mock authenticated ID
const VIEWER_ID = 'me';
// Mock user data
const viewer = new User();
viewer.id = VIEWER_ID;
const usersById = {
[VIEWER_ID]: viewer,
};
// Mock campaign data
const campaignsById = {};
const campaignIdsByUser = {
[VIEWER_ID]: [],
};
let nextCampaignId = 0;
addCampaign('Campaign1');
addCampaign('Campaign2');
addCampaign('Campaign3');
addCampaign('Campaign4');
export function addCampaign(text) {
const campaign = new Campaign();
//campaign.complete = !!complete;
campaign.id = `${nextCampaignId++}`;
campaign.text = text;
campaignsById[campaign.id] = campaign;
campaignIdsByUser[VIEWER_ID].push(campaign.id);
return campaign.id;
}
export function getCampaign(id) {
return campaignsById[id];
}
export function getCampaigns(status = 'any') {
const campaigns = campaignIdsByUser[VIEWER_ID].map(id => campaignsById[id]);
if (status === 'any') {
return campaigns;
}
return campaigns.filter(campaign => campaign.complete === (status === 'completed'));
}
This is my schema.js file, again I have removed the Todo related items to make the document easier to read:
import {
GraphQLBoolean,
GraphQLID,
GraphQLInt,
GraphQLList,
GraphQLNonNull,
GraphQLObjectType,
GraphQLSchema,
GraphQLString,
} from 'graphql';
import {
connectionArgs,
connectionDefinitions,
connectionFromArray,
cursorForObjectInConnection,
fromGlobalId,
globalIdField,
mutationWithClientMutationId,
nodeDefinitions,
toGlobalId,
} from 'graphql-relay';
import {
Campaign,
addCampaign,
getCampaign,
getCampaigns,
User,
getViewer,
} from './database';
const {nodeInterface, nodeField} = nodeDefinitions(
(globalId) => {
const {type, id} = fromGlobalId(globalId);
if (type === 'User') {
return getUser(id);
} else if (type === 'Campaign') {
return getCampaign(id);
}
return null;
},
(obj) => {
if (obj instanceof User) {
return GraphQLUser;
} else if (obj instanceof Campaign) {
return GraphQLCampaign;
}
return null;
}
);
/**
* Define your own connection types here
*/
const GraphQLAddCampaignMutation = mutationWithClientMutationId({
name: 'AddCampaign',
inputFields: {
text: { type: new GraphQLNonNull(GraphQLString) },
},
outputFields: {
campaignEdge: {
type: GraphQLCampaignEdge,
resolve: ({localCampaignId}) => {
const campaign = getCampaign(localCampaignId);
return {
cursor: cursorForObjectInConnection(getCampaigns(), campaign),
node: campaign,
};
},
},
viewer: {
type: GraphQLUser,
resolve: () => getViewer(),
},
},
mutateAndGetPayload: ({text}) => {
const localCampaignId = addCampaign(text);
return {localCampaignId};
},
});
const GraphQLCampaign = new GraphQLObjectType({
name: 'Campaign',
description: 'Campaign integrated in our starter kit',
fields: () => ({
id: globalIdField('Campaign'),
text: {
type: GraphQLString,
description: 'Name of the campaign',
resolve: (obj) => obj.text,
}
}),
interfaces: [nodeInterface]
});
const {
connectionType: CampaignsConnection,
edgeType: GraphQLCampaignEdge,
} = connectionDefinitions({
name: 'Campaign',
nodeType: GraphQLCampaign,
});
const GraphQLUser = new GraphQLObjectType({
name: 'User',
fields: {
id: globalIdField('User'),
campaigns: {
type: CampaignsConnection,
args: {
...connectionArgs,
},
resolve: (obj, {...args}) =>
connectionFromArray(getCampaigns(), args),
},
totalCount: {
type: GraphQLInt,
resolve: () => getTodos().length,
},
completedCount: {
type: GraphQLInt,
resolve: () => getTodos('completed').length,
},
},
interfaces: [nodeInterface],
});
const Root = new GraphQLObjectType({
name: 'Root',
fields: {
viewer: {
type: GraphQLUser,
resolve: () => getViewer(),
},
node: nodeField,
},
});
This is my AddCampaignMutation.js file:
import Relay from 'react-relay';
export default class AddCampaignMutation extends Relay.Mutation {
static fragments = {
viewer: () => Relay.QL`
fragment on User {
id,
totalCount,
}
`,
};
getMutation() {
console.log('getMutation');
return Relay.QL`mutation{addCampaign}`;
}
getFatQuery() {
console.log('getFatQuery');
return Relay.QL`
fragment on AddCampaignPayload #relay(pattern: true) {
campaignEdge,
viewer {
campaigns,
},
}
`;
}
getConfigs() {
console.log('getConfigs');
return [{
type: 'RANGE_ADD',
parentName: 'viewer',
parentID: this.props.viewer.id,
connectionName: 'campaigns',
edgeName: 'campaignEdge',
rangeBehaviors: ({orderby}) => {
if (orderby === 'newest') {
return 'prepend';
} else {
return 'append';
}
},
//rangeBehaviors: ({status}) => {
// if (status === 'completed') {
// return 'ignore';
// } else {
// return 'append';
// }
//},
}];
}
getVariables() {
console.log('getVariables');
return {
text: this.props.text,
};
}
getOptimisticResponse() {
console.log('getOptimisticResponse');
return {
// FIXME: totalCount gets updated optimistically, but this edge does not
// get added until the server responds
campaignEdge: {
node: {
text: this.props.text,
},
},
viewer: {
id: this.props.viewer.id,
totalCount: this.props.viewer.totalCount + 1,
},
};
}
}
And finally this is the app file that contains my text input and the call to AddCampaignMutation:
import AddTodoMutation from '../mutations/AddTodoMutation';
import AddCampaignMutation from '../mutations/AddCampaignMutation';
import TodoListFooter from './TodoListFooter';
import TodoTextInput from './TodoTextInput';
import React from 'react';
import Relay from 'react-relay';
class TodoApp extends React.Component {
_handleTextInputSave = (text) => {
debugger;
this.props.relay.commitUpdate(
new AddTodoMutation({text, viewer: this.props.viewer})
);
};
_campaignHandleTextInputSave = (text) => {
debugger;
this.props.relay.commitUpdate(
new AddCampaignMutation({text, viewer: this.props.viewer})
);
};
render() {
const hasTodos = this.props.viewer.totalCount > 0;
return (
<div>
<section className="todoapp">
<header className="header">
<TodoTextInput
autoFocus={true}
className="new-campaign"
onSave={this._campaignHandleTextInputSave}
placeholder="Campaign name"
/>
<h1>
todos
</h1>
<TodoTextInput
autoFocus={true}
className="new-todo"
onSave={this._handleTextInputSave}
placeholder="What needs to be done?"
/>
</header>
{this.props.children}
{hasTodos &&
<TodoListFooter
todos={this.props.viewer.todos}
viewer={this.props.viewer}
/>
}
</section>
</div>
);
}
}
export default Relay.createContainer(TodoApp, {
fragments: {
viewer: () => Relay.QL`
fragment on User {
totalCount,
${AddTodoMutation.getFragment('viewer')},
${AddCampaignMutation.getFragment('viewer')},
${TodoListFooter.getFragment('viewer')},
}
`,
},
});
Well I feel a bit silly but I found out what the problem was. I didn't realise that my schema.json file was not updating!
If anyone has a similar problem make sure that the schema.json file is up to date by running the following command to rebuild it:
npm run-script update-schema