Syncing (or pre-populating) Firebase data into Angular 5 FormArrays? - javascript

Using Angular 5, AngularFire2, and Firestore, I have a set of items that I want listed out in a grid that will be editable from the grid itself.
In the template, the grid is iterating over a FormArray inside a FormGroup.
<form [formGroup]="form">
<table class="table">
<thead class="thead-dark">
<tr>
<th scope="col">Serial Number</th>
<th></th>
</tr>
</thead>
<tbody formArrayName="devices">
<tr *ngFor="let device of form.controls.devices.controls; let i = index" [formGroupName]="i">
<th scope="row"><input type="text" class="form-control" formControlName="serialNumber"></th>
<td><button type="button" class="btn-sm btn-danger" (click)="deleteDevice(i)">Delete</button></td>
</tr>
</tbody>
</table>
</form>
In my Angular component, I'm calling buildForm() inside ngOnInit() to initially generate the form to give the template something to work with. If I don't do this, I get errors in the console.
I'm then generating the form again in the collection's snapshotChanges() in order to make sure the data is synced properly between Firebase and the FormArray.
If I don't do this, I'll get weird behavior such as rows remaining in the grid even after the item has been deleted or extra rows appearing once I add in another item.
#Component({
selector: 'app-home',
templateUrl: './home.component.html',
styleUrls: ['./home.component.css']
})
export class HomeComponent implements OnInit {
devicesCol: any;
devices: any;
serialNumber: string;
form: any;
constructor(private afs: AngularFirestore, private fb: FormBuilder) { }
ngOnInit() {
this.buildForm(); // Generate the form initially to give the template something to work with
this.devicesCol = this.afs.collection('devices', ref => ref.orderBy('deviceName'));
this.devicesCol.snapshotChanges().map(actions => {
return actions.map(a => {
const id = a.payload.doc.id;
const data = a.payload.doc.data();
return { id, data };
});
}).subscribe(result => {
this.form = this.fb.group({
devices: this.fb.array([]);
});
var devicesForm = this.form.get('devices');
this.devices.forEach((element) => {
devicesForm.push(this.fb.group({ 'id': element.id, 'serialNumber': element.data.serialNumber, 'deviceName': element.data.deviceName }));
}
});
}
buildForm() {
this.form = this.fb.group({
devices: this.fb.array([]);
})
}
addDevice() {
this.afs.collection('devices').add({ 'serialNumber': this.serialNumber, 'deviceName': '' }); // Add to firebase
}
deleteDevice(id) {
var devices = this.form.get('devices');
var deviceID = devices.at(id).get('id').value;
this.afs.doc('devices/' + deviceID).delete(); // Remove from firebase
devices.removeAt(id); // Remove from FormArray (probably unnecessary since the form will be rebuilt in snapshotChanges()
}
}
While this does work, I'm not sure it's best way to go about it. Is there a better way to sync data between Firebase and a FormArray?

Related

Cannot read property of undefined when calling method as a callback

I'm new to OOP and angular.
currently, I want to use reusable table with pagination that makes a request API if page change (pagination inside table component).
the problem is when I access my method using callback from table component (Child) I got undefined.
but when I try to move pagination to MasterGudang (Parent) Components it's work.
I don't really understand what's going on.
Error undefined
but here some code.
table.component.ts
import { Subject } from 'rxjs';
#Component({
selector: 'ngx-table-custom',
templateUrl: './table.component.html',
styleUrls: ['./table.component.scss']
})
export class TableComponent implements OnInit {
constructor() { }
#Input() items: any;
#Input() callback: any;
#Input() columns: [];
p: number = 1;
#ContentChild('action', { static: false }) actionRef: TemplateRef<any>;
ngOnInit(): void {
this.items = new Subject();
this.items.next();
}
onChangePage = (evt) => {
this.callback()
}
Gudang.component.ts
import { MasterGudangService } from '../../../../#core/services/master-service/menu-gudang/gudang/masterGudang.service';
#Component({
selector: "ngx-gudang",
templateUrl: './gudang.component.html',
styleUrls: ['./gudang.component.scss'],
})
#Injectable({
providedIn: 'root'
})
export class GudangComponent implements OnInit {
constructor(
public masterGudangService: MasterGudangService
) {
console.log(masterGudangService)
}
tableData: [];
isEdit: boolean = false;
currentPage: number = 1;
ngOnInit(): void {
this.getList();
}
getList (page?: number) {
this.masterGudangService.getPgb(page? page: this.currentPage).subscribe(response => {
const { data: { content, totalElements, size, number } } = response;
this.tableData = Object.assign({
data: content,
total: totalElements,
size: size,
number: number
});
});
}
}
And here I passing my function which is getList to table component
gudang.component.html
<ngx-table-custom [callback]="getList" [columns]="column" [items]="tableData">
<ng-template let-item #action>
<div class="row">
<button nbButton status="success" (click)="open(dialog, item, true)" class="mx-2" size="tiny"><nb-icon icon="edit"></nb-icon></button>
<button nbButton status="danger" (click)="onDelete(item)" size="tiny"><nb-icon icon="trash"></nb-icon></button>
</div>
</ng-template>
</ngx-table-custom>
MasterGudangService.ts
import { HttpClient } from '#angular/common/http';
import { Injectable } from '#angular/core';
#Injectable({
providedIn: 'root'
})
export class MasterGudangService {
constructor(private http: HttpClient) { }
getPgb (page: number = 1, perPage: number = 10) :any {
return this.http.get(`my-api-url/pgb?page=${page}&size=${perPage}`)
}
}
table.component.html
<div class="row">
<div class="col-12">
<table class="table table-md table-striped">
<thead>
<tr style="background-color: #3366ff; color: #fff;">
<th *ngFor="let column of columns" class="text-basic">{{ column.value }}</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let item of items.data | paginate: { itemsPerPage: 10, currentPage: p, totalItems: items.total }; index as idx;">
<td *ngFor="let column of columns">
<div *ngIf="column.key === 'number';"><b class="text-basic">{{ idx + 1 }}</b></div>
<div *ngIf="column.key !== 'action' && !isNested(column.key);" class="text-basic">{{ item[column.key] }}</div>
<div *ngIf="isNested(column.key);" class="text-basic">{{ getKeys(item, column.key) }}</div>
<!-- <div *ngIf="column.key === 'action; action_container"></div> -->
<ng-template [ngIf]="column.key === 'action'" #action_content>
<ng-container
*ngIf="actionRef"
[ngTemplateOutlet]="actionRef"
[ngTemplateOutletContext]="{$implicit:item}">
</ng-container>
</ng-template>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="row">
<div class="col-12" align="center">
<pagination-controls (pageChange)="onChangePage($event)"></pagination-controls>
</div>
</div>
The context of Gudang.component.ts will not be available using callback from table component.
The proper way to implement should be passing the event instead of passing function for callback
table.component.ts
#Output() pageChange = new EventEmitter()
onChangePage = (evt) => {
this.pageChange.emit(evt);
}
gudang.component.html
<ngx-table-custom (pageChange)="getList($event)" [columns]="column" [items]="tableData">
...
</ngx-table-custom>
based on the error, it seems like masterGudangService is null at the time you are trying to access it. Adding this code might help you eliminate the error and at least debug what is going on and get a step further.
ngOnInit(): void {
if(this.masterGudangService)
this.getList();
else
console.log('service not defined!');
}
You could define a helper Method in GudangComponent
getListCallback() {
return this.getList.bind(this);
}
and use it here
<ngx-table-custom [callback]="getListCallback()" [columns]="column" [items]="tableData">

LocalStorage within Angular Application not working as intended

In my Angular application. I have an array which is getting objects pushed to it from an rest api. The array is called playlist=[] and it is being shared across the components with a service called playlist service. Also within this service are two functions. One to save the playlist to localStorage and one to get it from localStorage. So what is happening is when I save the playlist, it saves to local storage fine. Even if I refresh they still are in the localstorage. So when I use my app in a single session (not refreshing the browser) the savePlaylist() method adds objects to the playlist array, it does not overwrite them (So that is fine). However if I refresh the page add items and then save - the items are overwritten, when they should be added to the localstorage ones that are already there saved. Is this possible? Is this to do with sessionStorage? Any Ideas? My code so far is:
playlist.service.ts
import { Injectable } from '#angular/core';
import { BehaviorSubject } from 'rxjs';
#Injectable({
providedIn: 'root'
})
export class PlaylistService {
public playlist = [];
getPlaylist() {
if (localStorage.getItem('playlist') == null) {
this.playlist = [];
} else {
this.playlist = JSON.parse(localStorage.getItem('playlist'));
}
}
savePlaylist() {
// first save the data
localStorage.setItem('playlist', JSON.stringify(this.playlist));
// get what is saved afterwords
this.playlist = JSON.parse(localStorage.getItem('playlist'));
console.log('Saved', this.playlist);
}
constructor() {
}
}
playlist.ts (Where it should show the save playlist)
import { Component, OnInit } from '#angular/core';
import { PlaylistService } from '../../../services/playlist.service';
import { faSave } from '#fortawesome/free-solid-svg-icons';
#Component({
selector: 'app-playlist-view',
templateUrl: './playlist-view.component.html',
styleUrls: ['./playlist-view.component.scss']
})
export class PlaylistViewComponent implements OnInit {
faSave = faSave;
constructor(private list: PlaylistService) { }
ngOnInit() {
this.list.getPlaylist();
}
}
playlist.html
<app-header></app-header>
<div class="container">
<table class="table mt-3 mb-3">
<thead class="thead-light">
<tr>
<th>Artwork</th>
<th>Artist</th>
<th>Title</th>
<th>Genre</th>
<th>Price</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let user of list.playlist">
<td><img src="{{user.artworkUrl60}}"></td>
<td>{{user.artistName}}</td>
<td>{{user.collectionName}}</td>
<td>{{user.primaryGenreName}}</td>
<td>{{user.collectionPrice}}</td>
</tr>
</tbody>
</table>
</div>
<app-footer></app-footer>

ERROR ReferenceError: $ is not defined at angular datatables [duplicate]

This question already has answers here:
JQuery - $ is not defined
(36 answers)
Closed 4 years ago.
I'm trying to add Paging and sorting to my table but I got this error , howerver I follow all the steps which listed here
http://l-lin.github.io/angular-datatables/#/getting-started.
I already check the previous problem but I did't work with me
I install all its dependencies
Here's the code of the component :-
import { Component, OnInit, OnDestroy } from '#angular/core';
import { ProductService } from '../../service/product-service.service';
import { Subscription, Subject } from 'rxjs';
#Component({
selector: 'app-admin-products',
templateUrl: './admin-products.component.html',
styleUrls: ['./admin-products.component.css']
})
export class AdminProductsComponent implements OnInit, OnDestroy {
products: any[];
filteredProducts: any[];
subscribtion: Subscription;
dtOptions: DataTables.Settings = {};
dtTrigger: Subject<any> = new Subject();
constructor(private productService: ProductService) {
this.subscribtion = productService.getAll().
// We take a copy of Products and Assigned to filteredProducts
subscribe(
products => {
this.filteredProducts = this.products = products;
this.dtTrigger.next();
}
);
}
ngOnInit() {
this.dtOptions = {
pagingType: 'full_numbers',
pageLength: 5,
processing: true
};
}
filter(queryStr: string) {
// console.log(this.filteredProducts);
if (queryStr) {
this.filteredProducts = this.products.
filter(p => p.payload.val().title.toLowerCase().includes(queryStr.toLowerCase()));
} else {
this.filteredProducts = this.products;
}
}
ngOnDestroy(): void {
// to UnSubscribe
this.subscribtion.unsubscribe();
}
}
Here's the code of the the HTML :-
I follow also all the steps here
<p>
<a routerLink="/admin/products/new" class="btn btn-primary">New Product</a>
</p>
<p>
<input type="text"
#query
(keyup)="filter(query.value)"
placeholder="Search ..." class="form-control">
</p>
<table
datatable [dtOptions]="dtOptions"
[dtTrigger]="dtTrigger" class="table" >
<thead class="thead-dark">
<tr>
<th scope="col">Title</th>
<th scope="col">Price</th>
<th scope="col">Edit</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let product of filteredProducts">
<td>{{ product.payload.val().title }}</td>
<td>{{ product.payload.val().price }}</td>
<td>
<a [routerLink]="['/admin/products/', product.key]">Edit</a>
</td>
</tr>
</tbody>
</table>
$ not defined mostly means you are not including JQuery.
try adding: to your program
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js" type="text/javascript"></script>
<script src="https://cdn.datatables.net/1.10.16/js/jquery.dataTables.min.js" type="text/javascript"></script>
source

Sort table from observable in Angular 6

I'm doing an app in Angular 6, where I'm getting data from JSON API to table. I have to sort this data's after click on header.
I don't know, how should I do it. I am a beginner at Angular, I am asking for your understanding
My .ts code
export class AppComponent implements OnInit {
readonly ROOT_URL = '...';
datas: Observable<Data[]>;
constructor(private http: HttpClient) {
}
sortTable(parm) {
this.datas.subscribe(item => item.sort((a: any, b: any) => {
return a[parm] - b[parm];
}));
}
getDatas() {
this.datas = this.http.get<Data[]>(this.ROOT_URL);
}
ngOnInit() {
this.getDatas();
}
}
Interface
export interface Data {
  long: string;
  perc: number;
  price: number;
}
And HTML Code
<div class="col-md-6">
  <table class="table">
    <thead class="thead-dark">
      <tr>
        <th scope="col" (click)="sortTable(long)">Nazwa</th>
        <th scope="col">Cena</th>
        <th scope="col">Zmiana(24h)</th>
      </tr>
    </thead>
    <tbody *ngFor="let data of datas | async" style="font-size: 12px;">
      <tr>
        <td>{{ data.long }}</td>
        <td>{{ data.price }} $</td>
        <td>{{ data.perc }} %</td>
      </tr>
    </tbody>
  </table>
</div>
You can remove async pipe, and subscribe to data with subscription function. So use sort function of Array.
...
datas: Array<Data>;
...
sortTable(parm) {
// you can use one of this solutions, but I recomend localeCompare
// this.datas.sort((a, b)=>a[parm] > b[parm]);
this.datas.sort((a, b)=> a[parm].localeCompare(b[parm]) );
}
getDatas() {
this.http.get<Data[]>(this.ROOT_URL).subscribe(it =>this.datas = it)
}
<tbody *ngFor="let data of datas" style="font-size: 12px;">
You can achieve ascending and descending sort using as below
sortTable(param){
this.filteredData.sort((a, b)=> {
return -1;
});
}

Angular2 : render a component without its wrapping tag

I am struggling to find a way to do this. In a parent component, the template describes a table and its thead element, but delegates rendering the tbody to another component, like this:
<table>
<thead>
<tr>
<th>Name</th>
<th>Time</th>
</tr>
</thead>
<tbody *ngFor="let entry of getEntries()">
<my-result [entry]="entry"></my-result>
</tbody>
</table>
Each myResult component renders its own tr tag, basically like so:
<tr>
<td>{{ entry.name }}</td>
<td>{{ entry.time }}</td>
</tr>
The reason I'm not putting this directly in the parent component (avoiding the need for a myResult component) is that the myResult component is actually more complicated than shown here, so I want to put its behaviour in a separate component and file.
The resulting DOM looks bad. I believe this is because it is invalid, as tbody can only contain tr elements (see MDN), but my generated (simplified) DOM is :
<table>
<thead>
<tr>
<th>Name</th>
<th>Time</th>
</tr>
</thead>
<tbody>
<my-result>
<tr>
<td>Bob</td>
<td>128</td>
</tr>
</my-result>
</tbody>
<tbody>
<my-result>
<tr>
<td>Lisa</td>
<td>333</td>
</tr>
</my-result>
</tbody>
</table>
Is there any way we can get the same thing rendered, but without the wrapping <my-result> tag, and while still using a component to be sole responsible for rendering a table row ?
I have looked at ng-content, DynamicComponentLoader, the ViewContainerRef, but they don't seem to provide a solution to this as far as I can see.
You can use attribute selectors
#Component({
selector: '[myTd]'
...
})
and then use it like
<td myTd></td>
You need "ViewContainerRef" and inside my-result component do something like this:
.html:
<ng-template #template>
<tr>
<td>Lisa</td>
<td>333</td>
</tr>
</ng-template>
.ts:
#ViewChild('template', { static: true }) template;
constructor(
private viewContainerRef: ViewContainerRef
) { }
ngOnInit() {
this.viewContainerRef.createEmbeddedView(this.template);
}
you can try use the new css display: contents
here's my toolbar scss:
:host {
display: contents;
}
:host-context(.is-mobile) .toolbar {
position: fixed;
/* Make sure the toolbar will stay on top of the content as it scrolls past. */
z-index: 2;
}
h1.app-name {
margin-left: 8px;
}
and the html:
<mat-toolbar color="primary" class="toolbar">
<button mat-icon-button (click)="toggle.emit()">
<mat-icon>menu</mat-icon>
</button>
<img src="/assets/icons/favicon.png">
<h1 class="app-name">#robertking Dashboard</h1>
</mat-toolbar>
and in use:
<navigation-toolbar (toggle)="snav.toggle()"></navigation-toolbar>
Attribute selectors are the best way to solve this issue.
So in your case:
<table>
<thead>
<tr>
<th>Name</th>
<th>Time</th>
</tr>
</thead>
<tbody my-results>
</tbody>
</table>
my-results ts
import { Component, OnInit } from '#angular/core';
#Component({
selector: 'my-results, [my-results]',
templateUrl: './my-results.component.html',
styleUrls: ['./my-results.component.css']
})
export class MyResultsComponent implements OnInit {
entries: Array<any> = [
{ name: 'Entry One', time: '10:00'},
{ name: 'Entry Two', time: '10:05 '},
{ name: 'Entry Three', time: '10:10'},
];
constructor() { }
ngOnInit() {
}
}
my-results html
<tr my-result [entry]="entry" *ngFor="let entry of entries"><tr>
my-result ts
import { Component, OnInit, Input } from '#angular/core';
#Component({
selector: '[my-result]',
templateUrl: './my-result.component.html',
styleUrls: ['./my-result.component.css']
})
export class MyResultComponent implements OnInit {
#Input() entry: any;
constructor() { }
ngOnInit() {
}
}
my-result html
<td>{{ entry.name }}</td>
<td>{{ entry.time }}</td>
See working stackblitz: https://stackblitz.com/edit/angular-xbbegx
Use this directive on your element
#Directive({
selector: '[remove-wrapper]'
})
export class RemoveWrapperDirective {
constructor(private el: ElementRef) {
const parentElement = el.nativeElement.parentElement;
const element = el.nativeElement;
parentElement.removeChild(element);
parentElement.parentNode.insertBefore(element, parentElement.nextSibling);
parentElement.parentNode.removeChild(parentElement);
}
}
Example usage:
<div class="card" remove-wrapper>
This is my card component
</div>
and in the parent html you call card element as usual, for example:
<div class="cards-container">
<card></card>
</div>
The output will be:
<div class="cards-container">
<div class="card" remove-wrapper>
This is my card component
</div>
</div>
Another option nowadays is the ContribNgHostModule made available from the #angular-contrib/common package.
After importing the module you can add host: { ngNoHost: '' } to your #Component decorator and no wrapping element will be rendered.
Improvement on #Shlomi Aharoni answer. It is generally good practice to use Renderer2 to manipulate the DOM to keep Angular in the loop and because for other reasons including security (e.g. XSS Attacks) and server-side rendering.
Directive example
import { AfterViewInit, Directive, ElementRef, Renderer2 } from '#angular/core';
#Directive({
selector: '[remove-wrapper]'
})
export class RemoveWrapperDirective implements AfterViewInit {
constructor(private elRef: ElementRef, private renderer: Renderer2) {}
ngAfterViewInit(): void {
// access the DOM. get the element to unwrap
const el = this.elRef.nativeElement;
const parent = this.renderer.parentNode(this.elRef.nativeElement);
// move all children out of the element
while (el.firstChild) { // this line doesn't work with server-rendering
this.renderer.appendChild(parent, el.firstChild);
}
// remove the empty element from parent
this.renderer.removeChild(parent, el);
}
}
Component example
#Component({
selector: 'app-page',
templateUrl: './page.component.html',
styleUrls: ['./page.component.scss'],
})
export class PageComponent implements AfterViewInit {
constructor(
private renderer: Renderer2,
private elRef: ElementRef) {
}
ngAfterViewInit(): void {
// access the DOM. get the element to unwrap
const el = this.elRef.nativeElement; // app-page
const parent = this.renderer.parentNode(this.elRef.nativeElement); // parent
// move children to parent (everything is moved including comments which angular depends on)
while (el.firstChild){ // this line doesn't work with server-rendering
this.renderer.appendChild(parent, el.firstChild);
}
// remove empty element from parent - true to signal that this removed element is a host element
this.renderer.removeChild(parent, el, true);
}
}
This works for me and it can avoid ExpressionChangedAfterItHasBeenCheckedError error.
child-component:
#Component({
selector: 'child-component'
templateUrl: './child.template.html'
})
export class ChildComponent implements OnInit {
#ViewChild('childTemplate', {static: true}) childTemplate: TemplateRef<any>;
constructor(
private view: ViewContainerRef
) {}
ngOnInit(): void {
this.view.createEmbeddedView(this.currentUserTemplate);
}
}
parent-component:
<child-component></child-component>

Categories