I'm trying to bind a (tap) event on a custom Angular component in nativescript.
I created a custom component called 'ns-custom' and tried to bind the (tap) event to it. But it doesn't work.
In the custom component I'm doing this:
<StackLayout>
<Label text="Title"></Label>
<Label text="some text"></Label>
</StackLayout>
import { Component, OnInit } from '#angular/core';
#Component({
selector: 'ns-custom',
templateUrl: './custom.component.html',
styleUrls: ['./custom.component.css']
})
export class CustomComponent{
constructor() { }
}
And in the parent element I'm doing this:
<ns-custom (tap)="onCustomComponentClick()"></ns-custom>
onCustomComponentClick() {
console.log('custom-component was tapped');
}
I expect the (tap) event to fire when I tap the custom component, but it does not. I built the same structure in pure Angular, and the (click) event does fire if put to a custom component.
I tried to propogate the (tap) event from the custom component like below, but then it fired twice (as expected because the tap event would propogate up to the parent component if I don't use event.stopPropagation()):
<StackLayout (tap)="emitTap()">
<Label text="Title"></Label>
<Label text="some text"></Label>
</StackLayout>
#Component({
selector: 'ns-custom',
templateUrl: './custom.component.html',
styleUrls: ['./custom.component.css']
})
export class CustomComponent{
#Output() tap = new EventEmitter();
emitTap() {
this.tap.emit();
}
}
And then catch the event on the parent component:
<ns-custom (tap)="onCustomComponentClick()"></ns-custom>
onCustomComponentClick() {
console.log('custom-component was tapped');
}
But now the tap event fires twice. I can solve it by changing the EventEmitter name to something other than 'tap' (for ex. #Output() childTapped = new EventEmitter(); and <ns-custom (childTapped)="onCustomComponentClick()"></ns-custom>) or pass the $event object on tap and then use event.stopPropagation(), but this is not elegant at all.
Any idea how to solve this simple problem in an elegant way?
This is basically answered by #mick-morely in the comments but I thought I would write up a more descriptive example and why I think it is a useful way of doing it.
You basically need to create a custom event for your custom component. While it seems tedious not to be able to re-use the tap event, it can actually improve your code quality, making it more descriptive.
So if I have a custom HoleComponent describing a "Hole", it can look like this:
Hole Template
<GridLayout rows="*" columns="*" (tap)="emitTap()">
<Label row="0" text="A very deep hole"></Label>
</GridLayout>
Hole Code
#Output() holeTap = new EventEmitter();
emitTap() {
this.holeTap.emit();
}
This Hole component can then be used by a parent like this:
Parent template
<Hole (holeTap)="onHoleTap()"></Hole>
Making the event explicitly named actually helps to make the code more readable imho. It also forces the developer to think more about the Domain they are working with which helps in conforming to the ubiquitous language if working with DDD is your thing.
Related
My problem:
Currently, I have a switch component I’m fiddling with.
My goal is to be able to click the switch component “ON” and have it stay switched “ON” when I navigate back to the home or another page.
Attempted solutions & experimentation:
I attached a method to the native (checkedChanged) event that comes “built-in” with NativeScript switch components.
Passed the built-in (checkedChanged) event, the event data from the switch click.
Then brute forced the switch component’s “checked” attribute, to permanently be TRUE or FALSE in two separate test cases on first user click, regardless of subsequent clicks...but whenever I navigate away from the page the switch in question is reset. I know this because my flag/counter is also reset. Every time I navigate away from the page.
Any tips on how to do this. Say I click a switch “ON” in one page in the app and navigate to another page in the same app. Or close out the app completely is there any way to make the state persist? Meaning that switch stays switched ON or OFF based on human interaction/toggling?
Resources:
https://docs.nativescript.org/angular/ui/ng-ui-widgets/switch
Use application-settings to store simple value persistently. There are also options like nativescript-localstorage / SQLite for storing data persistently. application-settings may be a good choice for simple key value pairs.
import { Component } from "#angular/core";
import { setBoolean, getBoolean } from "tns-core-modules/application-settings";
#Component({
selector: "Settings",
moduleId: module.id,
template: `
<ActionBar title="Settings" class="action-bar">
</ActionBar>
<GridLayout>
<ScrollView class="page">
<StackLayout class="form">
<GridLayout columns="*,auto">
<Label text="On / Off" class="h2"></Label>
<Switch [ngModel]="value" (ngModelChange)="onSwtichChange($event)"></Switch>
</GridLayout>
</StackLayout>
</ScrollView>
</GridLayout>
`
})
export class SettingsComponent {
value = getBoolean("my-value", false);
constructor() {
}
onSwtichChange(value: boolean) {
setBoolean("my-value", value);
}
}
Playground Sample
I need to localize TabViewItem title when TabView used as main navigation with Frame items (app generated with Nativescript Sidekick -> General -> TypeScript -> Tab Navigation).
For localization I use nativescript-localize plugin, with next initialization in app.ts for use in template:
import * as app from "tns-core-modules/application";
import { localize } from "nativescript-localize";
app.getResources().L = localize;
app.run({ moduleName: "app-root" });
And app-root.xml has next content:
<TabView androidTabsPosition="bottom">
<TabViewItem title="{{ L('catalog') }}">
<Frame defaultPage="pages/catalog/catalog-page"></Frame>
</TabViewItem>
<TabViewItem title="{{ L('notifications') }}">
<Frame defaultPage="pages/notifications/notifications-page"></Frame>
</TabViewItem>
</TabView>
If I use next construction {{ L('catalog') }} in final pages with initialized bindingContext then everything works as expected. But in top-level TabView this construction does not work. I think it is because not initialized bindingContext, but how can I initialize it for top-level TabView.
For testing and prototyping, I create Playground - top level TabView. In this Playground instead nativescript-localize plugin I use the function which return text, and as you can see it successfully executed in home-page.xml, but does not executed in app-root.xml.
I suspect it's an edge case bug, it seems the expressions are not processed until a bindingContext is set.
Just declaring bindingContext attribute in XML seems to solve the issue
<TabView androidTabsPosition="bottom" bindingContext="">
Updated Playground
Iam working the nativescript application.I have TabView with three tabs,in each tab i have progress indicator for showing api is loading and also hide the indicator after response came from api.In ios platform the indicator hide perfectly but android platform the indicator was not hide.How to solve the issue?
My TabView html code is:
<TabView [(ngModel)]="tabSelectedIndex" (selectedIndexChange)="onIndexChanged($event)" selectedTabTextColor="#40053e" androidTabsPosition="bottom" androidOffscreenTabLimit="0" iosIconRenderingMode="alwaysOriginal">
<StackLayout *tabItem="{title: 'Security', iconSource: 'res://lock'}">
<StackLayout>
<security></security>
</StackLayout>
</StackLayout>
<StackLayout *tabItem="{title: 'Personal', iconSource: 'res://boy'}">
<StackLayout>
<personal></personal>
</StackLayout>
</StackLayout>
<StackLayout *tabItem="{title: 'Payment', iconSource: 'res://card'}">
<StackLayout>
<payment></payment>
</StackLayout>
</StackLayout>
</TabView>
My TabViw ts code:
import { OnInit, Component } from "#angular/core";
import { TabView } from "tns-core-modules/ui/tab-view";
import { Page } from "ui/page";
#Component({
moduleId: module.id,
selector: "profiletab",
templateUrl: 'profiletab.component.html',
styleUrls: ['profiletab.component.css']
})
export class ProfileTabComponent implements OnInit {
private tabSelectedIndex:number;
private selectedTabName;
tabview:TabView;
constructor(private page:Page) {
this.tabSelectedIndex = 0;
this.selectedTabName = "Security Info";
}
ngOnInit(){
// this.tabview=<TabView>this.page.getViewById("tabcontrol");
// this.tabview.androidOffscreenTabLimit=1;
}
onIndexChanged(args){
let tabView = <TabView>args.object;
switch (tabView.selectedIndex) {
case 0:
this.selectedTabName = "Security Info";
break;
case 1:
this.selectedTabName = "Personal Info";
break;
case 2:
this.selectedTabName = "Payment Info";
break;
default:
break;
}
}
}
I will create indicator inside the tab1,tab2,tab3 like:
ngOnInit(){
this.loadCountries();
}
loadCountries(){
var _this = this;
if (!this.conn.getConnectionStatus()) {
Toast.makeText("Please check your internet connection").show();
return;
}
Indicator.showIndicator("Loading Please Wait...");
this.authenticationService.Get_Countries_List().then(function(response){
_this.renderData(response);
}
});
}
renderData(values){
this.authenticationService.Get_States_List(_this.dataService.userProfile.contact[0].country).then(function(response){
Indicator.hideIndicator();
}
}
You seem to create different instances while showing & hiding indicator. You must call hide of same LoadingIndicator instance on which you did call show.
Source: https://github.com/NathanWalker/nativescript-loading-indicator/issues/58
Also here is an example that demonstrates how you can do this without a plugin and always keep reference of current loading indicator no matter how many times you call show / hide simultaneously. You may even apply similar logic to maintain your LoadingIndicator instance with plugin. Refer ui-helper.ts and home-page.ts.
FYI, by nature of platform iOS seems to use a singleton which is why it works there even if you create multiple LoadingIndicator. But with Android a new progress dialog is created every time when you call show, so you must call hide on same instance.
Also as you are using Angular, I would suggest you to take advantage of framework features like Http Interceptors so you wouldn't have to create / handle indicator for each tab or each api call separately.
I have a tap event in my code. Its parent(not an immediate parent) also has tap event. In Android, everything works fine. But in ios, the event is propagating upwards to that parent. How to stop that event propagation (such as event.stopPropagation() in javascript).
Sample XML code:
<StackLayout tap="newsDetails" data-args="{{ $value }}">
<StackLayout orientation="horizontal" >
<Label text="Share" class="icon" tap="shareNews" data-args="{{ $value }}"/>
</StackLayout>
</StackLayout>
Note: I am using NativeScript core
Thank you.
You can use the boolean property isUserInteractionEnabled, however, this won't solve the issue as it will stop the user interaction for all nested elements as well. It is simply not good practice to have multiple tap events in the same area.
I had the same issue on android, here's a simple solution i came up with, not elegant but works :
private ignoreTap = false;
shareNews(){
this.ignoreTap = true;
// ...
}
newsDetails(){
if (this.ignoreTap) { this.ignoreTap = false; return; }
// ...
}
The flag ignoreTap will stop the root function execution.
HI for example below is my view
<ScrollView>
<StackLayout>
<StackLayout *ngFor="let kid of kids" id="kidList">
<StackLayout orientation="horizontal" class="some-class">
<Lable text="{{ kid.fname }} {{ kid.lname }}"></Lable>
<Lable text="{{ kid.age }} years ago"></Lable>
</StackLayout>
</StackLayout>
</StackLayout>
</ScrollView>
I want to append the more data getting from server to 'kidList' when scroll reaches to bottom of screen in {N} aAngular2.
It's very hard to me build the layouts and adding childs in 'js' like below(KidInformation has more data).
let stackLayout = new StackLayout();
stackLayout.addChild('something newly constructed with JS')
Is there a way we can do in My Component just by adding child as view by passing local parameters to view , I mean like in below way
let kidListStacklayout = view.getViewById(this.page, 'kidList');
kidListStacklayout.addChild('views/kid/kid-item.html')
and kid-item.html will look like
<StackLayout orientation="horizontal" class="some-class">
<Lable text="{{ kid.fname }} {{ kid.lname }}"></Lable>
<Lable text="{{ kid.age }} years ago"></Lable>
</StackLayout>
The stock list view supports what you want. Don't use a scrollview and adding more layouts to the screen. This will cause lag and other issues. A listview recycles UI view components to reduce overhead of the layout growing in size. You want to use the loadMore event on the list view. https://docs.nativescript.org/cookbook/ui/list-view
Of course as the comment above ^^^ the free UI suite from telerik provides the RadListView which also supports infinite scrolling with a loadMore event.
Find out how to do it ( using Angular & TypeScript) :
import { ScrollView, ScrollEventData } from "ui/scroll-view";
#Component({...})
class ComponentClass implements OnInit, AfterViewInit {
#ViewChild("scrollid") scrollView: ElementRef;
ngOnInit(){
let scrollv : ScrollView = <ScrollView>this.scrollView.nativeElement;
scrollv.on(ScrollView.scrollEvent, function (args: ScrollEventData) {
if(scrollv.scrollableHeight === args.scrollY){
console.log("load more items here !!! ");
}
});
}
}
The scrollv.scrollableHeight gets updated by itself.
Tested on android emulator only. Must work on both Plateforms.