How do you expose methods from NativeScript custom components? - nativescript

I want to expose a method from a custom component in a NativeScript project. Here is the conceptual setup:
MyComponent.xml
<StackLayout loaded="loaded"> ...UI markup... </StackLayout>
MyComponent.ts
export function loaded(args) { //do stuff };
export function myCustomMethod() { //do more stuff };
MyPage.xml
<Page xmlns:widget="..." loaded="loaded">
<widget:MyComponent id="myComponent" />
</Page>
MyPage.ts
export function loaded(args) {
let page = <Page>args.object;
let widget = page.getViewById("myComponent");
widget.myCustomMethod(); //<--THIS DOES NOT WORK
}
Question: How do I properly expose the myCustomMethod on the custom component so that the hosting page can access and call it via JavaScript?
By default, when trying to get a reference to the custom component on the page, a reference to the custom component's layout container is returned (in this example, a StackLayout).
So, as a workaround, I am doing this:
MyComponent.ts
export function loaded(args) {
let stackLayout = <StackLayout>args.object; //Layout container
stackLayout.myCustomMethod = myCustomMethod; //Attach custom method to StackLayout
}
It's a bit of a hack, but it's working. Is there any better way to expose custom component methods short of creating a super class for the layout container?
UPDATE
To be more accurate, the custom component has instance properties, so the eventual solution would need to support a scenario like...
MyComponent.ts
let isStarted = false;
export function loaded(args){ // do stuff };
export function myCustomMethod() { isStarted = true; };

As the method you want to reuse is not coupled to your customCompoenent, you can create a common file and require it where needed and reuse the exposed methods accordingly. Or you can directly import your MyComponent.ts and reuse its exported methods (not so good idea as you will probably have access to all exposed methods of your MyCompoennt.ts like onLoaded, onNavigationgTo, etc.)
For example:
in your navigator.ts (or if you prefer to your MyComponent.ts)
import frame = require("ui/frame");
export function navigateBack() {
frame.goBack();
}
and then reuse it where needed like this:
MyPage.ts
import navigatorModule = require("navigator");
export function goBack(args: observable.EventData) {
navigatorModule.goBack(); // we are reusing goBack() from navigator.ts
}
Another applicable option is to use MVVM pattern and expose your methods via the binding context.

Related

Use scrollToAnchor from ViewportScroller in angular 6

I cannot figure out how to use the function ViewportScroller.scrollToAnchor(string anchor).
First of all - how do I define an anchor in my html? I may be confusing anchors, routerlinks and fragments.
My code which is based on fragments as of now:
export class ItemsOverviewPage implements OnInit {
public items: Item[];
constructor(private route: ActivatedRoute,
private scroller: ViewportScroller) {}
public async ngOnInit(): Promise<void> {
const fragment = await this.route.fragment.first().toPromise();
if (fragment !== undefined || fragment !== null) {
this.scroller.scrollToAnchor(fragment);
}
}
}
The html is something like
<ion-card mode="md"
*ngFor="let i of items"
routerDirection="forward"
id="{{ i.title) }}">
</ion-card>
How can I refer to the id? Or should I do an <a>...</a> around whatever I want to scroll to?
I am navigating to the page like:
this.router.navigate(['/items'], { fragment: item.title });
I don't think you can do it that way.
It seems that, when you are using a ngFor, the scrolling to the anchor gets called before the DOM is finalized.
So, in your ngOnInit you can get the fragment, but won't be able to find the anchor, as the ngFor has not completed yet.
One way way to do what you want, could be to use a route parameter rather than a fragment.You can retrieve the parameter in the ngOnInit of your ItemsOverviewPageComponent and store it in a variable (e.g. _fragment), and then scroll to the anchor in the ngAfterViewInit() hook using document.getElementById(this._fragment).scrollIntoView();
Another option, could be using navigationExtras.
Even better, if you need to pass data and potentially complex objects via routes, would be to set up a service that stores the data and then inject it into the components
For more information see ActivatedRoute, NavigationExtras

Dynamically adding custom elements to DOM Aurelia [duplicate]

It seems Aurelia is not aware when I create and append an element in javascript and set a custom attribute (unless I am doing something wrong). For example,
const e = document.createElement('div');
e.setAttribute('custom-attr', 'some value');
body.appendChild(e);
Is there a way to make Aurelia aware of this custom attribute when it gets appended?
A little background: I am creating an app where the user can select their element type (e.g. input, select, checkbox etc.) and drag it around (the dragging is done in the custom attribute). I thought about creating a wrapper <div custom-attr repeat.for="e of elements"></div> and somehow render the elements array, but this seemed inefficient since the repeater will go through all the elements everytime I push a new one and I didn't not want to create a wrapper around something as simple as a text input that might be created.
You would have to manually trigger the Aurelia's enhance method for it to register the custom attributes or anything Aurelia related really. And you also have to pass in a ViewResources object containing the custom attribute.
Since this isn't as straight forward as you might think, I'll explain it a bit.
The enhance method requires the following parameters for this scenario:
Your HTML as plain text (string)
The binding context (in our scenario, it's just this)
A ViewResources object that has the required custom attribute
One way to get access to the ViewResources object that meets our requirements, is to require the custom attribute into your parent view and then use the parent view's ViewResources. To do that, require the view inside the parent view's HTML and then implement the created(owningView, thisView) callback in the controller. When it's fired, thisView will have a resources property, which is a ViewResources object that contains the require-d custom attribute.
Since I am HORRIBLE at explaining, please look into the example provided below.
Here is an example how to:
app.js
import { TemplatingEngine } from 'aurelia-framework';
export class App {
static inject = [TemplatingEngine];
message = 'Hello World!';
constructor(templatingEngine, viewResources) {
this._templatingEngine = templatingEngine;
}
created(owningView, thisView) {
this._viewResources = thisView.resources;
}
bind() {
this.createEnhanceAppend();
}
createEnhanceAppend() {
const span = document.createElement('span');
span.innerHTML = "<h5 example.bind=\"message\"></h5>";
this._templatingEngine.enhance({ element: span, bindingContext: this, resources: this._viewResources });
this.view.appendChild(span);
}
}
app.html
<template>
<require from="./example-custom-attribute"></require>
<div ref="view"></div>
</template>
Gist.run:
https://gist.run/?id=7b80d2498ed17bcb88f17b17c6f73fb9
Additional resources
Dwayne Charrington has written an excellent tutorial on this topic:
https://ilikekillnerds.com/2016/01/enhancing-at-will-using-aurelias-templating-engine-enhance-api/

Nativescript Switch not to fire initial binding

My view is
<Switch checked="{{ active }}" propertyChange="onCheckChange"/>
exports.onCheckChange = function(args)
{
//Api Service call
}
Actually I am binding the active value by API call and the issue is that onCheckChange gets executed during the initial binding with false value, so whenever I initially set the active==true by api service call and load the page, the onCheckChange is executed with checked==false, can anyone give me an idea about this please.
Note: Beginner in Nativescript
I battled with the checked property a lot so I opted for two-way binding, which behaves as expected:
// test.xml
<Switch [(ngModel)]="isUnicorn"></Switch>
// test.ts
isUnicorn: boolean = true;
......
if (this.isUnicorn) {
console.log("It is a unicorn");
}
Note that to get two-way binding to work you need to import NativeScriptFormsModule in app.module.ts or applicable module for instance:
// app.module.ts
import { NativeScriptFormsModule } from "nativescript-angular/forms";
......
#NgModule({
imports: [
NativeScriptFormsModule,
......
],
exports: [
NativeScriptFormsModule,
......
],
......
The two-way data-binding (described by leoncc) might be specific to the Angular NativeScript.
Here's a workaround without the two-way data binding, hopefully it will be easier to port to the plain NativeScript if needs be.
In the controller we can get the state of the Switch with a ViewChild query:
checked = true;
#ViewChild ('switch') private switch: ElementRef;
switched() {
let switch: Switch = this.switch.nativeElement;
this.checked = switch.checked}
And in the template we should invoke the switched change handler:
<Switch #switch [checked]="checked" (checkedChange)="switched()" class="switch"></Switch>

How to Include events (not event-plugin) in Ractive initialization/defaults?

I've read through the Ractive Documentation and I'm scratching my head a bit, because it seems like the default events initialization option allows me to do something - create new eventtypes - far more complex than what i need but conversely, there's no hook for the simpler, (more common?) task of defining default events
Could someone advise on how to provide global events that could be fired for traditional DOM events?
Example:
I have a 3 Component application page. I want to define a getOptions event, such that any <select on-click='getOptions'>...</select> will be handled by the same function. I don't want to have to define that function in each component.
My intuition would have been to do the following:
Ractive.events['getOptions'] = function(event){
//logic for getting the options for the value in event.keypath
}
or, if i wanted a true default that could be overridden...
Ractive.default.events['getOptions'] = function(event){
//logic for getting the options for the value in event.keypath
}
but my understanding of the documentation, is that Ractive.events and Ractive.default.events do not provide this, but rather provide a way to define new event plugins, that depend on a separate mechanism for getting fired:
Ractive.events.getoptions = function(node,fire){
//here goes logic for interacting with DOM event listeners, etc
}
//and then i would need to do this
ractive = Ractive.extend({...});
ractive.on('someOtherEventName',function(event){
//logic for getting the options for the value in event.keypath
});
//and then I could do this...
<select on-getoptions='someOtherEventName'>...</select>
but what would fire the getoptions in this case - from the template, rather than js ractive.fire()?
Would something like <select on-getoptions='someOtherFunction' on-click=getoptions>...</select> work? That seems very strange to me. Do I understand the concept correction? If not, what am i missing?
Is there a simple way to achieve the first example?
Ractive.events refers to custom events for mediating between the dom and the template:
Ractive.events.banana = function( node, fire ) { ... };
<div on-banana="doSomething()"/>
The handler for the event can either be the name of an event to fire, or a method on the component instance.
In your case, I think defining a method on the Ractive.prototype would be the best way to have a common handler:
Ractive.prototype.getOptions = function( /* pass in arguments */ ){
// and/or this.event will give you access
// to current event and thus context
// you can also override this method in components and
// call this base method using this._super(..)
}
// now any ractive instance can use:
<select on-click="getOptions(data)">...</select>
An event based approach usually entails letting the root instance or common parent in the view hierarchy handle same event across child components:
var app = new Ractive({
template: "<componentA/><componentB/>",
oninit(){
this.on( '*.getOptions', ( event, arg ) => {
// any child component (at any depth)
// that fires a "getOptions" event will
// end up here
});
}
});
// in component A or B:
<select on-click="getOptions">...</select>
UPDATE: If you wanted to assign an event handler to the prototype, so in essence every component is pre-wired to handle an event of a set name, you could do:
Ractive.prototype.oninit = function(){
this.on( 'getOptions', ( event ) => {
// handle any "getOptions" event that happens in the instance
});
}
Just be aware that you must call this._super(); in any component in which you also implement oninit:
var Component = Ractive.extend({
oninit() {
// make sure we call the base or event listener won't happen!
this._super();
// do this component instances init work...
}
}

How do I add default events to Marionette's Marionette.View base type?

I am currently extending Marionette's base Marionette.View type with the method I named quickClick. I'm doing this to
config/marionette/view.js
(function() {
define([
'backbone.marionette'
],
function(Marionette){
return _.extend(Backbone.Marionette.View.prototype, {
quickClick: function(e) {
$(e.target).get(0).click();
}
});
});
}).call(this);
This allows me to call this method from any view I create without having to redefine it per view. Great!
Here's a trimmed down view with the events object still in place:
(function() {
define([
'backbone.marionette',
'app',
'templates'
],
function(Marionette, App, templates){
// Define our Sub Module under App
var List = App.module("SomeApp");
List.Lessons = Backbone.Marionette.ItemView.extend({
events: {
'tap .section-container p.title': 'quickClick'
}
});
// Return the module
return List;
});
}).call(this);
In case your wondering, tap is an event I'm using from Hammer.js because this is a mobile app. So, in order to circumvent the 300ms click event delay on iOS and Android, I'm manually triggering a click event when the tap event fires on certain elements.
Now, all of this is working just fine, and I felt it was necessary to describe this in detail, so that an answer could be given with context.
My problem is having to define the events object. I don't mind at all for elements as specific as the one above, .section-container p.title. But, I would like to register a tap event for all <a> tags within every view. It doesn't make sense to keep defining this event in each view I create
events: {
'tap .section-container p.title': 'quickClick',
// I don't want to add this to every single view manually
'tap a': 'quickClick'
}
Instead, of adding this to every view, I thought I would just add an events object to the config/marionette/view.js file where I added a method to the Marionette.View prototype.
Here's what I did
(function() {
define([
'backbone.marionette'
],
function(Marionette){
return _.extend(Backbone.Marionette.View.prototype, {
events: {
'tap a': 'quickClick'
},
quickClick: function(e) {
$(e.target).get(0).click();
}
});
});
}).call(this);
Of course, that doesn't work. The events object is overridden each time I need to add events that only apply to that view. Btw, tap a does work when my view does not have its' own events object.
So, my question is: How do I add default events to Marionette's Marionette.View base type?
"Of course, that doesn't work. The events object is overridden each time I need to add events that only apply to that view."
Yes, that seems to be the problem. Here is the part of Marionette that does the event delegation:
// internal method to delegate DOM events and triggers
_delegateDOMEvents: function(events){
events = events || this.events;
if (_.isFunction(events)){ events = events.call(this); }
var combinedEvents = {};
var triggers = this.configureTriggers();
_.extend(combinedEvents, events, triggers);
Backbone.View.prototype.delegateEvents.call(this, combinedEvents);
},
One possible solution could be overwriting this (private!) part of Marionette - but it could probably change in new versions of Marionette and you'd always have to make sure that things still work. So this is bad.
But you could do something like this in your subviews.:
events: _.extend(this.prototype.events, {
'tap .section-container p.title': 'quickClick'
})
If this makes sense for only one 'global' event is another question.
Or you could define an abstract View Class, which does something like that
events: _.extend({'tap a': 'quickClick'}, this.my_fancy_events)
and also defines the quickClick method and then use this view for all you subviews. They then define their events not in 'events' but in 'my_fancy_events'.
When extending the views I occasionally find myself in situation when I need to add some extra calls in 'initialize' as well as extend 'events' property to include some new calls.
In my abstract view I have a function:
inheritInit: function(args) {
this.constructor.__super__.initialize.apply(this, args);
this.events = _.extend(this.constructor.__super__.events, this.eventsafter);
},
Then, in an extended view, I can call
initialize: function(options) {
this.inheritInit(arguments)
//..some extra declarations...
}
and also I can use 'events' property in a regular way.

Resources