RadListView doesn't display items - angular2-nativescript

I am having much difficulties implementing RadListView in my application. According to documentation the ListView accepts items as an ObservableArray, but regardless of whether that or plain array is used, items are not being displayed. I prepared a sample app in the Playground
https://play.nativescript.org/?template=play-ng&id=39xE2X&v=11
Component is like this:
import { Component, ViewChild, OnInit, ChangeDetectorRef, ElementRef } from "#angular/core";
import { GestureTypes, PanGestureEventData, TouchGestureEventData } from "tns-core-modules/ui/gestures";
import { ObservableArray } from "tns-core-modules/data/observable-array";
#Component({
selector: "ns-app",
moduleId: module.id,
templateUrl: "./app.component.html",
})
export class AppComponent implements OnInit {
public gridData = []; //new ObservableArray<any>();
constructor(
private cd: ChangeDetectorRef
) {
this.feedTestData();
}
ngOnInit() {
}
ngAfterViewInit() {
}
public detectChanges() {
this.cd.detectChanges();
}
private feedTestData() {
let data = [
{
description: 'line 1',
},
{
description: 'line 2',
},
];
this.gridData.splice(0, 0, data);
}
}
and template like this:
<GridLayout tkExampleTitle tkToggleNavButton>
<RadListView [items]="gridData">
<ng-template tkListItemTemplate let-item="item">
<StackLayout orientation="vertical">
<Label [text]="description"></Label>
</StackLayout>
</ng-template>
</RadListView>
</GridLayout>
What's perplexing me even more is that in my real application RadListView displays something, but does it on its own, completely ignoring my template. I can literally leave nothing inside RadListView, but it would still display items

I have updated your playground and it is working now.
https://play.nativescript.org/?template=play-ng&id=39xE2X&v=12
You were trying to access the description in ng-template while it should be item.description
<Label [text]="item.description"></Label>
Also for the testing purpose, I am creating an Onservable array from your data. this.gridData = new ObservableArray(data); in your feedTestData function.

Related

Displaying component property from record provider is undefined

I am creating an ionic app. In this modal, I want a select with options populated from my Provider (called recordProvider). categories should hold an array of objects from the recordProvider.
The name property of these objects is what goes in the select.
I am able to log categories immediately after it is assigned from recordsProvider and it shows all the proper records perfectly. However, the next line logs the length at 0. Most importantly, the UI errors with "Cannot read property 'name' of undefined"
Why does categories have this inconsistent value?
If it is just an issue of timing and categories will have the correct data in a moment, why isn't it updated in the UI? Isn't that the whole get with Angular?
How do I fix it?
Modal ts
import { Component } from '#angular/core';
import { IonicPage, NavController, NavParams,ViewController } from 'ionic- angular';
import { RecordsProvider } from './../../providers/records/records';
#IonicPage()
#Component({
selector: 'page-add-modal',
templateUrl: 'add-modal.html',
})
export class AddModalPage {
categories:object[] = [];
constructor(public navCtrl: NavController, public navParams: NavParams, public viewCtrl : ViewController, public recordProvider: RecordsProvider) {
}
ngOnInit() {
this.categories = this.recordProvider.getAllExpenseCategories();
console.log(this.categories);
console.log(this.categories.length);
}
public closeModal(){
this.viewCtrl.dismiss();
}
}
Modal HTML
<ion-content padding>
<h1 (click)="getCat()">Hello</h1>
<p>{{categories[0].name}}</p>
<ion-item>
<ion-label>categories</ion-label>
<ion-select>
<ion-option ng-repeat="obj of categories" value="{{obj.name}}">{{obj.name}}</ion-option>
</ion-select>
</ion-item>
</ion-content>
EDIT RecordsProvider
import { HttpClient } from '#angular/common/http';
import { Injectable } from '#angular/core';
import { Storage } from '#ionic/storage';
#Injectable()
export class RecordsProvider {
getAllExpenseCategories(){
let categories = [];
this.storage.forEach( (value, key, index)=>{
if(key.indexOf("Exp") == 0){
categories.push(value);
}
});
return categories;
}
}
Ionic Storage (localForage) uses async API, so I would make sure you write your methods with it accordingly, I would re-write the getAllExpenseCategories to leverage promise which is returned by storage:
getAllExpenseCategories(){
let categories = [];
this.storage.forEach( (value, key, index)=>{
if(key.indexOf("Exp") == 0){
categories.push(value);
}
}).then(()=>{
return categories;
})
}
In your case it seems like your method was returning empty array to the component, before storage completed its forEach cycle.
Let me know if this helped

Inheriting validation using ControlValueAccessor in Angular

I have a custom form control component (it is a glorified input). The reason for it being a custom component is for ease of UI changes - i.e. if we change the way we style our input controls fundamentally it will be easy to propagate change across the whole application.
Currently we are using Material Design in Angular https://material.angular.io
which styles controls very nicely when they are invalid.
We have implemented ControlValueAccessor in order to allow us to pass a formControlName to our custom component, which works perfectly; the form is valid/invalid when the custom control is valid/invalid and the application functions as expected.
However, the issue is that we need to style the UI inside the custom component based on whether it is invalid or not, which we don't seem to be able to do - the input that actually needs to be styled is never validated, it simply passes data to and from the parent component.
COMPONENT.ts
import { Component, forwardRef, Input, OnInit } from '#angular/core';
import {
AbstractControl,
ControlValueAccessor,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
ValidationErrors,
Validator,
} from '#angular/forms';
#Component({
selector: 'app-input',
templateUrl: './input.component.html',
styleUrls: ['./input.component.css'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => InputComponent),
multi: true
}
]
})
export class InputComponent implements OnInit, ControlValueAccessor {
writeValue(obj: any): void {
this._value = obj;
}
registerOnChange(fn: any): void {
this.onChanged = fn;
}
registerOnTouched(fn: any): void {
this.onTouched = fn;
}
setDisabledState?(isDisabled: boolean): void {
this.disabled = isDisabled;
}
get value() {
return this._value;
}
set value(value: any) {
if (this._value !== value) {
this._value = value;
this.onChanged(value);
}
}
#Input() type: string;
onBlur() {
this.onTouched();
}
private onTouched = () => {};
private onChanged = (_: any) => {};
disabled: boolean;
private _value: any;
constructor() { }
ngOnInit() {
}
}
COMPONENT.html
<ng-container [ngSwitch]="type">
<md-input-container class="full-width" *ngSwitchCase="'text'">
<span mdPrefix><md-icon>lock_outline</md-icon> </span>
<input mdInput placeholder="Password" type="text" [(ngModel)]="value" (blur)="onBlur()" />
</md-input-container>
</ng-container>
example use on page:
HTML:
<app-input type="text" formControlName="foo"></app-input>
TS:
this.form = this.fb.group({
foo: [null, Validators.required]
});
You can get access of the NgControl through DI. NgControl has all the information about validation status. To retrieve NgControl you should not provide your component through NG_VALUE_ACCESSOR instead you should set the accessor in the constructor.
#Component({
selector: 'custom-form-comp',
templateUrl: '..',
styleUrls: ...
})
export class CustomComponent implements ControlValueAccessor {
constructor(#Self() #Optional() private control: NgControl) {
this.control.valueAccessor = this;
}
// ControlValueAccessor methods and others
public get invalid(): boolean {
return this.control ? this.control.invalid : false;
}
public get showError(): boolean {
if (!this.control) {
return false;
}
const { dirty, touched } = this.control;
return this.invalid ? (dirty || touched) : false;
}
}
Please go through this article to know the complete information.
Answer found here:
Get access to FormControl from the custom form component in Angular
Not sure this is the best way to do it, and I'd love someone to find a prettier way, but binding the child input to the form control obtained in this manner solved our issues
In addition: Might be considered dirty, but it does the trick for me:
let your component implement the Validator interface.
2 In the validate function you use the controlcontainer to get to the outer formcontrol of your component.
Track the status of the parent form control (VALID/INVALID) by using a variable.
check for touched. and perform validation actions on your fields only when touched is true and the status has changed.

How to create a custom form component by extending BaseInput in ionic2

I want to create a custom form input component in ionic2, by extending BaseInput. But it doesn't rendered, and I can't find it on the DOM.
import { Component, ElementRef, OnDestroy, Optional, Renderer,
ViewEncapsulation } from "#angular/core";
import { Config, Form, Item } from "ionic-angular";
import { BaseInput } from "ionic-angular/util/base-input";
import { NG_VALUE_ACCESSOR } from "#angular/forms";
#Component({
selector: 'my-checkbox',
template:
'<p>aaaaa</p>',
host: {
'[class.checkbox-disabled]': '_disabled'
},
providers: [ { provide: NG_VALUE_ACCESSOR, useExisting: MyCheckboxComponent, multi: true } ],
encapsulation: ViewEncapsulation.None,
})
export class MyCheckboxComponent extends BaseInput<any> implements OnDestroy {
constructor(form: Form, config: Config, elementRef: ElementRef, renderer: Renderer, #Optional() item: Item) {
super(config, elementRef, renderer, 'my-checkbox', [], form, item, null);
}
}
The code is copy from src/component/checkbox/checkbox.ts and make a little changes.
I had the same problem. My component did not get render within <ion-item> parent element. I fixed it by adding item-content directive
<ion-item>
<ion-label>Label</ion-label>
<my-checkbox item-content></my-checkbox>
</ion-item>

Fire Nativescript tabitem event when tabitem gets selected

I am using a Nativescript (Angular 2) TabView with two TabItems. The XML is divided intro three files. One that holds the TabView and two others for each TabItem. Therefore I also have three TypeScript components.
At the moment I am loading data in the second TabItem's onInit method. The problem is that this action already happens when the first TabItem of the TabView is being displayed/loaded.
What is the best practice to load this data only when the second TabItem is selected?
This is my (shortened) code:
home.page.html:
<ActionBar title="Home"></ActionBar>
<TabView #tabview (selectedIndexChanged)="tabIndexChanged($event)" toggleNavButton>
<StackLayout *tabItem="{title: 'Tab 1'}">
<tab1></tab1>
</StackLayout>
<StackLayout *tabItem="{title: 'Tab 2'}">
<tab2></tab2>
</StackLayout>
</TabView>
home.page.ts:
import {Component} from "#angular/core";
#Component({
selector: "home-page",
templateUrl: "./pages/home/home.page.html",
providers: []
})
export class HomePage {
public activeTab: string;
public constructor() {
}
public tabIndexChanged(e: any) {
switch (e.newIndex) {
case 0:
console.log(`Selected tab index: ${e.newIndex}`);
break;
case 1:
console.log(`Selected tab index: ${e.newIndex}`);
break;
default:
break;
}
}
}
tab1.tab.html:
<StackLayout orientation="vertical" class="p-20">
<Label text="Tab 1"></Label>
</StackLayout>
tab1.tab.ts:
import { Component, OnInit } from "#angular/core";
#Component({
selector: "tab1",
templateUrl: "./pages/partials/tab1.tab.html",
providers: []
})
export class Tab1 implements OnInit {
public constructor() {}
public ngOnInit() {
console.log("init Tab 1");
}
}
tab2.tab.html:
<StackLayout orientation="vertical" class="p-20">
<Label text="Tab 2"></Label>
</StackLayout>
tab2.tab.ts:
import { Component, OnInit } from "#angular/core";
#Component({
selector: "tab2",
templateUrl: "./pages/partials/tab2.tab.html",
providers: []
})
export class Tab2 implements OnInit {
public constructor() {}
public ngOnInit() {
console.log("init Tab 2");
this.getSomeDataViaHttp();
}
private getSomeDataViaHttp() {
//getting data from an API
}
}
Is there an Angular 2 / Nativescript event other than onInit that would help here?
Or should I use the method tabIndexChanged in the home.page.ts for that?
Or put all the logic and the XML for the TabView back into one xml file and one ts file?
What is best practice?
You could use a service and a Subject as followed.
Import the service file in all ts files (use the name and location you like):
import { NavService } from "./services/nav.service";
Make sure to import it also in your app.module.ts to generally load it:
import { NavService } from "./services/nav.service";
#NgModule({
declarations: [
AppComponent,
],
bootstrap: [AppComponent],
imports: [
],
providers: [
NavService
]
})
export class AppModule {}
Create the service file in the specified location with the following content:
import { Injectable } from "#angular/core";
import { Subject } from "rxjs";
#Injectable()
export class NavService {
private currentState = new Subject<any>();
constructor () {
}
setCurrentState(navPoint: number){
this.currentState.next(navPoint);
}
getCurrentState() {
return this.currentState.asObservable();
}
}
Change the tab2.tab.ts to the following:
import { Component, OnInit } from "#angular/core";
import { NavService } from "./services/nav.service";
#Component({
selector: "tab2",
templateUrl: "./pages/partials/tab2.tab.html",
providers: []
})
export class Tab2 implements OnInit {
public constructor(private _navService: NavService) {}
public ngOnInit() {
console.log("init Tab 2");
this._navService.getCurrentState().subscribe(
(state) => {
if (state == {{something}}) {
//write your code here which should be executed when state has the property {{something}}
this.getSomeDataViaHttp();
}
}
);
}
private getSomeDataViaHttp() {
//getting data from an API
}
}
Call the setCurrentState of the service in your home.page.ts:
import {Component} from "#angular/core";
import { NavService } from "./services/nav.service";
#Component({
selector: "home-page",
templateUrl: "./pages/home/home.page.html",
providers: []
})
export class HomePage {
public activeTab: string;
public constructor(private _navService: NavService) {
}
public tabIndexChanged(e: any) {
this._navService.setCurrentState(e.newIndex);
}
}
Take care that the "typeof" setting and getting the state is correct.

How do I programmatically set focus to dynamically created FormControl in Angular2

I don't seem to be able to set focus on a input field in dynamically added FormGroup:
addNewRow(){
(<FormArray>this.modalForm.get('group1')).push(this.makeNewRow());
// here I would like to set a focus to the first input field
// say, it is named 'textField'
// but <FormControl> nor [<AbstractControl>][1] dont seem to provide
// either a method to set focus or to access the native element
// to act upon
}
How do I set focus to angular2 FormControl or AbstractControl?
I made this post back in December 2016, Angular has progressed significantly since then, so I'd make sure from other sources that this is still a legitimate way of doing things
You cannot set to a FormControl or AbstractControl, since they aren't DOM elements. What you'd need to do is have an element reference to them, somehow, and call .focus() on that. You can achieve this through ViewChildren (of which the API docs are non-existent currently, 2016-12-16).
In your component class:
import { ElementRef, ViewChildren } from '#angular/core';
// ...imports and such
class MyComponent {
// other variables
#ViewChildren('formRow') rows: ElementRef;
// ...other code
addNewRow() {
// other stuff for adding a row
this.rows.first().nativeElement.focus();
}
}
If you wanted to focus on the last child...this.rows.last().nativeElement.focus()
And in your template something like:
<div #formRow *ngFor="let row in rows">
<!-- form row stuff -->
</div>
EDIT:
I actually found a CodePen of someone doing what you're looking for https://codepen.io/souldreamer/pen/QydMNG
For Angular 5, combining all of the above answers as follows:
Component relevant code:
import { AfterViewInit, QueryList, ViewChildren, OnDestroy } from '#angular/core';
import { Subscription } from 'rxjs/Subscription';
// .. other imports
export class MyComp implements AfterViewInit, OnDestroy {
#ViewChildren('input') rows: QueryList<any>;
private sub1:Subscription = new Subscription();
//other variables ..
// changes to rows only happen after this lifecycle event so you need
// to subscribe to the changes made in rows.
// This subscription is to avoid memory leaks
ngAfterViewInit() {
this.sub1 = this.rows.changes.subscribe(resp => {
if (this.rows.length > 1){
this.rows.last.nativeElement.focus();
}
});
}
//memory leak avoidance
ngOnDestroy(){
this.sub1.unsubscribe();
}
//add a new input to the page
addInput() {
const formArray = this.form.get('inputs') as FormArray;
formArray.push(
new FormGroup(
{input: new FormControl(null, [Validators.required])}
));
return true;
}
// need for dynamic adds of elements to re
//focus may not be needed by others
trackByFn(index:any, item:any){
return index;
}
The Template logic Looks like this:
<div formArrayName="inputs" class="col-md-6 col-12"
*ngFor="let inputCtrl of form.get('phones').controls;
let i=index; trackBy:trackByFn">
<div [formGroupName]="i">
<input #input type="text" class="phone"
(blur)="addRecord()"
formControlName="input" />
</div>
</div>
In my template I add a record on blur, but you can just as easily set up a button to dynamically add the next input field. The important part is that with this code, the new element gets the focus as desired.
Let me know what you think
This is the safe method recommend by angular
#Component({
selector: 'my-comp',
template: `
<input #myInput type="text" />
<div> Some other content </div>
`
})
export class MyComp implements AfterViewInit {
#ViewChild('myInput') input: ElementRef;
constructor(private renderer: Renderer) {}
ngAfterViewInit() {
this.renderer.invokeElementMethod(this.input.nativeElement,
'focus');
}
}
With angular 13, I did it this way:
import { Component, OnInit, Input } from '#angular/core';
import { FormGroup, Validators, FormControl, FormControlDirective, FormControlName } from '#angular/forms';
// This setting is required
const originFormControlNgOnChanges = FormControlDirective.prototype.ngOnChanges;
FormControlDirective.prototype.ngOnChanges = function ()
{
this.form.nativeElement = this.valueAccessor._elementRef.nativeElement;
return originFormControlNgOnChanges.apply(this, arguments);
};
const originFormControlNameNgOnChanges = FormControlName.prototype.ngOnChanges;
FormControlName.prototype.ngOnChanges = function ()
{
const result = originFormControlNameNgOnChanges.apply(this, arguments);
this.control.nativeElement = this.valueAccessor._elementRef.nativeElement;
return result;
};
#Component({
selector: 'app-prog-fields',
templateUrl: './prog-fields.component.html',
styleUrls: ['./prog-fields.component.scss']
})
export class ProgFieldsComponent implements OnInit
{
...
generateControls()
{
let ctrlsForm = {};
this.fields.forEach(elem =>
{
ctrlsForm[elem.key] = new FormControl(this.getDefaultValue(elem), this.getValidators(elem));
});
this.formGroup = new FormGroup(ctrlsForm);
}
...
validateAndFocus()
{
if (formGroup.Invalid)
{
let stopLoop = false;
Object.keys(formGroup.controls).map(KEY =>
{
if (!stopLoop && formGroup.controls[KEY].invalid)
{
(<any>formGroup.get(KEY)).nativeElement.focus();
stopLoop = true;
}
});
alert("Warn", "Form invalid");
return;
}
}
}
Reference:
https://stackblitz.com/edit/focus-using-formcontrolname-as-selector?file=src%2Fapp%2Fapp.component.ts
Per the #Swiggels comment above, his solution for an element of id "input", using his solution after callback:
this.renderer.selectRootElement('#input').focus();
worked perfectly in Angular 12 for an element statically defined in the HTML (which is admittedly different somewhat from the OP's question).
TS:
#ViewChild('licenseIdCode') licenseIdCodeElement: ElementRef;
// do something and in callback
...
this.notifyService.info("License Updated.", "Done.");
this.renderer.selectRootElement('#licenseIdCode').focus();
HTML:
<input class="col-3" id="licenseIdCode" type="text" formControlName="licenseIdCode"
autocomplete="off" size="40" />
If you are using Angular Material and your <input> is a matInput, you can avoid using .nativeElement and ngAfterViewInit() as follows:
Component Class
import { ChangeDetectorRef, QueryList, ViewChildren } from '#angular/core';
import { MatInput } from '#angular/material/input';
// more imports...
class MyComponent {
// other variables
#ViewChildren('theInput') theInputs: QueryList<MatInput>;
constructor(
private cdRef: ChangeDetectorRef,
) { }
// ...other code
addInputToFormArray() {
// Code for pushing an input to a FormArray
// Force Angular to update the DOM before proceeding.
this.cdRef.detectChanges();
// Use the matInput's focus() method
this.theInputs.last.focus();
}
}
Component Template
<ng-container *ngFor="iterateThroughYourFormArrayHere">
<input #theInput="matInput" type="text" matInput>
</ng-container>