How to add a custom event to a NativeScript UI plugin - plugins

What is required to define a custom event for a UI plugin in NativeScript?
What I'm trying to achieve is to trigger a foo event that works similar to the tap event on a Button and can be hooked into as follows:
<Page xmlns="http://schemas.nativescript.org/tns.xsd"
xmlns:fooplugin="nativescript-foo-plugin">
<StackLayout>
<Button text="Tap Me!" tap="{{ onTap }}" />
<fooplugin:FooPlugin foo="{{ onFoo }}" />
</StackLayout>
</Page>
What I've done essentially boils down to calling the notify function with the eventName value of foo from within the plugin code (ignoring memory leak considerations):
import * as view from 'ui/core/view';
export class FooPlugin extends view.View {
constructor() {
super();
setTimeout(() => {
this.notify({
eventName: 'foo',
object: this,
});
// also tried this._emit('foo');
}, 1000);
}
}
Is there something else that I'm missing and that I need to do to make this work?

create a property public static fooEvent="foo"
the name of the property is important it should be eventname+ Event now it should work.

create a property of your Event public static fooEvent="foo". The name is important! It has to be eventname + "Event"
overload the on-function in your declaration file index.d.ts or FooPlugin.d.ts
// Needed when on method is overriden.
on(eventNames: string, callback: (data: EventData) => void, thisArg?: any);

Related

dataRequested pass variable in XML

Just wondering how do I pass a this or other variables in dataRequested and dataReceived in XML events? Something like the below:
<Select items="{
path: '/Countries',
events: {
dataRequested: '.onCountriesRequested($source)',
dataReceived: '.onCountriesReceived($source)',
change: '.onCountriesChange'
}
}">
'this' is the scope of your controller on which your view.xml is attached.
You can directly control everything related to your Select UI element in the related file Detail.controller.js or App.controller.js.
Just add an event listener in your UI element such as:
change="onCountriesChange"
Then define the function "onCountriesChange" in the corresponding controller.js, for example:
onCountriesChange : function(oEvent) {console.log(this);}
Or you can simply bind 'this' to your event handler by attaching it at the end of your function, for example
this.oVariable1 = "Test";
oSelect.attachEvent('dataReceived', function (oEvent) {
//write your logic here using this.oVariable1 for example
}.bind(this));

Lifecycle for controlled textinputs

I was wondering if the mobx cycle is triggered on observable change only in stores outside of react components in HOClass operation
because there is a top level Provider to mobx stores which is triggering a react component re-render.
ReactDOM.render(<Provider { ...stores } >
<Routes />
</Provider>,
But does that mean I can't add observables directly to react components?
if so that is a bit unfortunate.
#observer
export default class Profile extends React.Component {
#observable name;
componentWillMount() {
this.name = this.props.name;
}
txtChange = (e) => {
this.name = e.target.value;
}
render() {
return (
<div>
<input type="text" onChange={this.txtChange} value={this.name}/>
</div>
)
}
for some reason on my app I cannot update the observable name. On some apps using mobx however I can. Why am I unable to change the value of this and trigger a react state update if the observable is attached to the React.Component itself?
Here I have forms updating in controlled observable fashion but when Provider 'ing my router on my current project under this setup no react rerender is triggered.
https://codepen.io/cheapsteak/pen/wzdvzQ
Help anyone?
There (was) a typo: the function is called txtChange but the component references this.txthange instead. Either way, here's a fixed working example: https://codepen.io/justnobody/pen/RxJmmY?editors=1010

Wrapping a FormControl in Angular (2+)

I'm trying to create a custom form control in Angular (v5). The custom control is essentially a wrapper around an Angular Material component, but with some extra stuff going on.
I've read various tutorials on implementing ControlValueAccessor, but I can't find anything that accounts for writing a component to wrap an existing component.
Ideally, I want a custom component that displays the Angular Material component (with some extra bindings and stuff going on), but to be able to pass in validation from the parent form (e.g. required) and have the Angular Material components handle that.
Example:
Outer component, containing a form and using custom component
<form [formGroup]="myForm">
<div formArrayName="things">
<div *ngFor="let thing of things; let i = index;">
<app-my-custom-control [formControlName]="i"></app-my-custom-control>
</div>
</div>
</form>
Custom component template
Essentially my custom form component just wraps an Angular Material drop-down with autocomplete. I could do this without creating a custom component, but it seems to make sense to do it this way as all the code for handling filtering etc. can live within that component class, rather than being in the container class (which doesn't need to care about the implementation of this).
<mat-form-field>
<input matInput placeholder="Thing" aria-label="Thing" [matAutocomplete]="thingInput">
<mat-autocomplete #thingInput="matAutocomplete">
<mat-option *ngFor="let option of filteredOptions | async" [value]="option">
{{ option }}
</mat-option>
</mat-autocomplete>
</mat-form-field>
So, on the input changing, that value should be used as the form value.
Things I've tried
I've tried a few ways of doing this, all with their own pitfalls:
Simple event binding
Bind to keyup and blur events on the input, and then notify the parent of the change (i.e. call the function that Angular passes into registerOnChange as part of implementing ControlValueAccessor).
That sort of works, but on selecting a value from the dropdown it seems the change events don't fire and you end up in an inconsistent state.
It also doesn't account for validation (e.g. if it's "required", when a value isn;t set the form control will correctly be invalid, but the Angular Material component won't show as such).
Nested form
This is a bit closer. I've created a new form within the custom component class, which has a single control. In the component template, I pass in that form control to the Angular Material component. In the class, I subscribe to valueChanges of that and then propagate the changes back to the parent (via the function passed into registerOnChange).
This sort of works, but feels messy and like there should be a better way.
It also means that any validation applied to my custom form control (by the container component) is ignored, as I've created a new "inner form" that lacks the original validation.
Don't use ControlValueAccessor at all, and instead just pass in the form
As the title says... I tried not doing this the "proper" way, and instead added a binding to the parent form. I then create a form control within the custom component as part of that parent form.
This works for handling value updates, and to an extent validation (but it has to be created as part of the component, not the parent form), but this just feels wrong.
Summary
What's the proper way of handling this? It feels like I'm just stumbling through different anti-patterns, but I can't find anything in the docs to suggest that this is even supported.
Edit:
I've added a helper for doing just this an angular utilities library I've started: s-ng-utils. Using that you can extend WrappedFormControlSuperclass and write:
#Component({
selector: 'my-wrapper',
template: '<input [formControl]="formControl">',
providers: [provideValueAccessor(MyWrapper)],
})
export class MyWrapper extends WrappedFormControlSuperclass<string> {
// ...
}
See some more documentation here.
One solution is to get the #ViewChild() corresponding to the inner form components ControlValueAccessor, and delegating to it in your own component. For example:
#Component({
selector: 'my-wrapper',
template: '<input ngDefaultControl>',
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => NumberInputComponent),
multi: true,
},
],
})
export class MyWrapper implements ControlValueAccessor {
#ViewChild(DefaultValueAccessor) private valueAccessor: DefaultValueAccessor;
writeValue(obj: any) {
this.valueAccessor.writeValue(obj);
}
registerOnChange(fn: any) {
this.valueAccessor.registerOnChange(fn);
}
registerOnTouched(fn: any) {
this.valueAccessor.registerOnTouched(fn);
}
setDisabledState(isDisabled: boolean) {
this.valueAccessor.setDisabledState(isDisabled);
}
}
The ngDefaultControl in the template above is to manually trigger angular to attach its normal DefaultValueAccessor to the input. This happens automatically if you use <input ngModel>, but we don't want the ngModel here, just the value accessor. You'll need to change DefaultValueAccessor above to whatever the value accessor is for the material dropdown - I'm not familiar with Material myself.
I'm a bit late to the party but here is what I did with wrapping a component which might accept formControlName, formControl, or ngModel
#Component({
selector: 'app-input',
template: '<input [formControl]="control">',
styleUrls: ['./app-input.component.scss']
})
export class AppInputComponent implements OnInit, ControlValueAccessor {
constructor(#Optional() #Self() public ngControl: NgControl) {
if (this.ngControl != null) {
// Setting the value accessor directly (instead of using the providers) to avoid running into a circular import.
this.ngControl.valueAccessor = this;
}
}
control: FormControl;
// These are just to make Angular happy. Not needed since the control is passed to the child input
writeValue(obj: any): void { }
registerOnChange(fn: (_: any) => void): void { }
registerOnTouched(fn: any): void { }
ngOnInit() {
if (this.ngControl instanceof FormControlName) {
const formGroupDirective = this.ngControl.formDirective as FormGroupDirective;
if (formGroupDirective) {
this.control = formGroupDirective.form.controls[this.ngControl.name] as FormControl;
}
} else if (this.ngControl instanceof FormControlDirective) {
this.control = this.ngControl.control;
} else if (this.ngControl instanceof NgModel) {
this.control = this.ngControl.control;
this.control.valueChanges.subscribe(x => this.ngControl.viewToModelUpdate(this.control.value));
} else if (!this.ngControl) {
this.control = new FormControl();
}
}
}
Obviously, don't forget to unsubscribe from this.control.valueChanges
I have actually been wrapping my head around this for a while and I figured out a good solution that is very similar (or the same) as Eric's.
The thing he forgot to account for, is that you can't use the #ViewChild valueAccessor until the view has actually loaded (See #ViewChild docs)
Here is the solution: (I am giving you my example which is wrapping a core angular select directive with NgModel, since you are using a custom formControl, you will need to target that formControl's valueAccessor class)
#Component({
selector: 'my-country-select',
templateUrl: './country-select.component.html',
styleUrls: ['./country-select.component.scss'],
providers: [{
provide: NG_VALUE_ACCESSOR,
useExisting: CountrySelectComponent,
multi: true
}]
})
export class CountrySelectComponent implements ControlValueAccessor, OnInit, AfterViewInit, OnChanges {
#ViewChild(SelectControlValueAccessor) private valueAccessor: SelectControlValueAccessor;
private country: number;
private formControlChanged: any;
private formControlTouched: any;
public ngAfterViewInit(): void {
this.valueAccessor.registerOnChange(this.formControlChanged);
this.valueAccessor.registerOnTouched(this.formControlTouched);
}
public registerOnChange(fn: any): void {
this.formControlChanged = fn;
}
public registerOnTouched(fn: any): void {
this.formControlTouched = fn;
}
public writeValue(newCountryId: number): void {
this.country = newCountryId;
}
public setDisabledState(isDisabled: boolean): void {
this.valueAccessor.setDisabledState(isDisabled);
}
}
NgForm is providing an easy way to manage your forms without injecting any data in a HTML form. Input data must be injected at the component level not in a classic html tag.
<form #myForm="ngForm" (ngSubmit)="onSubmit(myForm)>...</form>
Other way is to create a form component where all the data model is binded using ngModel ;)

ReactNative: In the navigation bar on the right button, click the event does not Working, an error

ReactNative:
I use react-navigation Component.
In the navigation bar on the right button, click the event is not Working
`_newCustomer() {
alert('点击新建');
}
static navigationOptions = {
title: '客户列表',
headerRight: (
<Button title={'添加客户'} onPress={this._newCustomer.bind(this)}/>
),
}
The Error:
undefined is not an object(evaluating 'this._newCustomer')
The problem is because you're trying to call this._newCustomer from a static function, and this is undefined, because you're in a static function.
If you look at the React Navigation Docs you'll see that in the examples they use anonymous functions. Do that, or call another static function instead.
static navigationOptions does not have capability to link your dynamic 'this' variable. Normally you need to create a custom Button component and use this component to deal with click event.
static navigationOptions = ({ navigation }) => {
return {
headerRight: (
<EventButton
navigation={navigation}
/>
),
};
};
and then
export const EventButton = (props) => {
let testButton = <TouchableHighlight onPress={() => props.navigation.navigate('CreateNewCustomer',{ name: 'CreateNewCustomer'})}>
</TouchableHighlight>
return testButton
}

Ionic2: use getActive() to change button color for the active page

I have a nav component which I am using on 4 pages, I want to be able to change the color of active page's button in the nav component. In Ionic app doc's for nav controller I found getActive() instance, but I can't figure out how to achieve the desired result with it. I'm using the following code to push to a new view.
viewPage2(){
this.navCtrl.push(Page2);
}
<button ion-button (click)="viewPage2()" color="dark" clear full>Page 2</button>
NavController getActive() returns the ViewController of the Active page.
Looking at the API of ViewController you could try using getContentRef():
this.navCtrl.getActive().contentRef().nativeElement.getElementById("button_id")
Once you have the element you could change the color.
Even though getting the html element by its id may work, modifying the DOM directly is not the recommended way to do things in Ionic.
First option:
If that's a custom component, you can always expose a public method in that component, and get the reference by using ViewChild
#Component({...})
export class NavCustomComponent {
public activePage: string = 'page1';
//...
public changeActivePage(pageName: string): void {
this.activePage = pageName;
}
// ...
}
And in your view:
<button ion-button (click)="viewPage2()" [color]="activePage === 'page2' ? 'light' : 'dark'" clear full>Page 2</button>
Then in the page where you're trying to modify the component:
#Component({...})
export class DemoPage {
#ViewChild(NavCustomComponent) navCustomComponent: NavCustomComponent;
}
and then use that reference to call that public method:
this.navCustomComponent.changeActivePage('page2');
Second option:
If that's not a custom component, or you just want to make things even simpler, you can just Events. Whereever you're defining the code of that nav component, (or in your app.component.ts file to make it global for the entire app) subscribe to the event:
public activePage: string = 'page1';
constructor(public events: Events, ...) {
events.subscribe('page:selected', (pageName) => {
this.activePage = pageName;
});
}
Again, in your view:
<button ion-button (click)="viewPage2()" [color]="activePage === 'page2' ? 'light' : 'dark'" clear full>Page 2</button>
And then in the component where you want to change the color, just publish that event:
events.publish('page:selected', 'page2');