A presentation at Frontend Connect in in Warsaw, Poland by Rowdy Rabouw
FrontendCon 2019 - @rowdyrabouw
Czy miałeś miły lunch?
Unleash your web skills on native!
Web developer mood coaster
web developer in natural habitat
web developer • HTML • CSS • JavaScript • Sass • Node Package Manager #FrontendCon 2019 - @rowdyrabouw 8/157
web developer on native iOS / Android
web developer & native • App stores • Provisioning files • Java or Kotlin for Android • Objective-C or Swift for iOS #FrontendCon 2019 - @rowdyrabouw 11/157
web developer with nativescript
web developer & nativescript • App stores • Provisioning files • NativeScript • HTML, CSS, JavaScript • Sass • Node Package Manager #FrontendCon 2019 - @rowdyrabouw 14/157
web developer ❤ nativescript
who?
Rowdy Rabouw » @rowdyrabouw » Gouda, The Netherlands » Freelance web and app developer » Lead developer Nationale-Nederlanden Pension App » Progress Developer Expert for Nativescript » I ❤ superhero movies #FrontendCon 2019 - @rowdyrabouw 18/157
Mobile App framework decision guide
Mobile App framework decision guide Do you want/need a native User Interface and native performance? No #FrontendCon 2019 - @rowdyrabouw Yes 23/157
Mobile App framework decision guide Do you want/need a native User Interface and native performance? No Yes Phonegap / Cordova with Ionic • WebView • DOM to manipulate • HTML styled like native #FrontendCon 2019 - @rowdyrabouw 24/157
Mobile App framework decision guide Do you want/need a native User Interface and native performance? No Phonegap / Cordova with Ionic Yes continue • WebView • DOM to manipulate • HTML styled like native #FrontendCon 2019 - @rowdyrabouw 25/157
Mobile App framework decision guide Do you have too much money and time? Yes #FrontendCon 2019 - @rowdyrabouw No 26/157
Mobile App framework decision guide Do you have too much money and time? Yes No Native iOS and Android • Twice the work #FrontendCon 2019 - @rowdyrabouw 27/157
Mobile App framework decision guide Do you have too much money and time? Yes Native iOS and Android No continue • Twice the work #FrontendCon 2019 - @rowdyrabouw 28/157
Mobile App framework decision guide Do you potentially want/need to share code with the web? Or do you want/need to use web technologies? No #FrontendCon 2019 - @rowdyrabouw Yes 29/157
Mobile App framework decision guide Do you potentially want/need to share code with the web? Or do you want/need to use web technologies? No Yes Xamarin • .NET or C# • Cross compiling • Bindings to access native APIs #FrontendCon 2019 - @rowdyrabouw 30/157
Mobile App framework decision guide Do you potentially want/need to share code with the web? Or do you want/need to use web technologies? No Yes Flutter • Dart • Cross compiling #FrontendCon 2019 - @rowdyrabouw 31/157
Mobile App framework decision guide Do you potentially want/need to share code with the web? Or do you want/need to use web technologies? No Flutter Yes continue • Dart • Cross compiling #FrontendCon 2019 - @rowdyrabouw 32/157
Mobile App framework decision guide Do you want to use modern JavaScript? No #FrontendCon 2019 - @rowdyrabouw Yes 33/157
Mobile App framework decision guide Do you want to use modern JavaScript? No Yes Titanium • No ES6/ES2015 support • Can’t use NPM • Old MVC framework (Alloy) #FrontendCon 2019 - @rowdyrabouw 34/157
Mobile App framework decision guide Do you want to use modern JavaScript? No Titanium Yes continue • No ES6/ES2015 support • Can’t use NPM • Old MVC framework (Alloy) #FrontendCon 2019 - @rowdyrabouw 35/157
Mobile App framework decision guide Do you know and like React? Yes #FrontendCon 2019 - @rowdyrabouw No 36/157
Mobile App framework decision guide Do you know and like React? Yes No React Native • React • Bridge to access native APIs #FrontendCon 2019 - @rowdyrabouw 37/157
Mobile App framework decision guide Do you know and like React? Yes No React Native • React • Bridge to access native APIs • Hold my beer! #FrontendCon 2019 - @rowdyrabouw 38/157
Mobile App framework decision guide Do you know and like React? Yes React Native No continue • React • Bridge to access native APIs • Hold my beer! #FrontendCon 2019 - @rowdyrabouw 39/157
NATIVESCRIPT
NATIVESCRIPT
What is NativeScript? • Open source framework for building truly native mobile apps • JavaScript, markup (XML/HTML) and CSS • Native code inside your JavaScript if you want and dare • Cross Platform: one codebase for iOS and Android • Backed by software company Progress • Android 4.2 or a later stable official release • iOS 9.0 or later stable official release #FrontendCon 2019 - @rowdyrabouw 43/157
docs.nativescript.org
nativescript.org/get-the-nativescript-book
nativescripting.com
nativescriptcommunity.slack.com
nativescript.org/nativescript-sidekick
play.nativescript.org
markup like on the web
Markup http://2xr.nl/markup https://docs.nativescript.org/ui/components <ActionBar title=”Native elements”/> <StackLayout> <Button text=”Button” tap=”{{ onButtonTap }}”/> <Switch checked=”false”/> <SegmentedBar items=”{{ segmentedBarItems }}”/> <Progress value=”0” maxValue=”100”/> <Slider value=”0” minValue=”0” maxValue=”100”/> <DatePicker year=”2018” month=”1” day=”1” minDate=”1970-01-01” maxDate=”2100-12-31”/> </StackLayout> #FrontendCon 2019 - @rowdyrabouw 52/157
TextField https://docs.nativescript.org/api-reference/modules/ui_text_field.textfield <TextField/> <TextField text=”“/> <TextField hint=”Enter your name”/> #FrontendCon 2019 - @rowdyrabouw 54/157
TextField: autocapitalization https://docs.nativescript.org/api-reference/modules/ui_text_field.textfield <TextField autocapitalizationType=”allCharacters”/> <TextField autocapitalizationType=”sentences”/> <TextField autocapitalizationType=”words”/> #FrontendCon 2019 - @rowdyrabouw 55/157
TextField: autocapitalization https://docs.nativescript.org/api-reference/modules/ui_text_field.textfield <TextField autocapitalizationType=”allCharacters”/> <TextField autocapitalizationType=”sentences”/> <TextField autocapitalizationType=”words”/> <TextField autocapitalizationType=”none”/> #FrontendCon 2019 - @rowdyrabouw 56/157
TextField: autocorrect https://docs.nativescript.org/api-reference/modules/ui_text_field.textfield <TextField autocorrect=”true”/> <TextField autocorrect=”false”/> #FrontendCon 2019 - @rowdyrabouw 57/157
TextField: keyboardType http://2xr.nl/keyboardType https://docs.nativescript.org/api-reference/modules/ui_enums.keyboardtype <TextField keyboardType=”number”/> <TextField keyboardType=”datetime”/> <TextField keyboardType=”phone”/> <TextField keyboardType=”email”/> <TextField keyboardType=”url”/> #FrontendCon 2019 - @rowdyrabouw 58/157
TextField: more attributes https://docs.nativescript.org/api-reference/modules/ui_text_field.textfield <TextField textAlignment=”“/> <TextField visibility=”“/> <TextField width=”“/> <TextField maxLength=”“/> #FrontendCon 2019 - @rowdyrabouw 60/157
Layouts https://docs.nativescript.org/ui/layouts #FrontendCon 2019 - @rowdyrabouw 61/157
Layouts https://docs.nativescript.org/ui/layouts #FrontendCon 2019 - @rowdyrabouw 62/157
DockLayout https://docs.nativescript.org/ui/layouts <DockLayout height=”100%” stretchLastChild=”true”> <Label <Label <Label <Label text=”1” text=”2” text=”3” text=”4” dock=”top”/> dock=”bottom”/> dock=”right”/> dock=”left”/> </DockLayout> <!-1 + 2 have fixed height 3 has fixed width 4 will get all remaining space —> #FrontendCon 2019 - @rowdyrabouw 63/157
GridLayout https://docs.nativescript.org/ui/layouts <GridLayout rows=”100, auto, *” columns=”100, auto, *”> <Label text=”1” row=”0” <Label text=”2” row=”0” colSpan=”2”/> <Label text=”3” row=”1” rowSpan=”2”/> <Label text=”4” row=”1” <Label text=”5” row=”1” <Label text=”6” row=”2” <Label text=”7” row=”2” </GridLayout> #FrontendCon 2019 - @rowdyrabouw col=”0”> col=”1” col=”0” col=”1”/> col=”2”/> col=”1”/> col=”2”/> 64/157
Cascading Style Sheets
Cascading Style Sheets https://docs.nativescript.org/ui/styling • a large subset of CSS properties is supported • device-independent pixels • application-wide, page-specific or inline • platform-specific possible • animations • SASS #FrontendCon 2019 - @rowdyrabouw 67/157
{N} Core Themes https://docs.nativescript.org/ui/theme • ready to use color schemes • tailored for iOS and Android #FrontendCon 2019 - @rowdyrabouw
NativeScript Theme Builder https://www.nativescriptthemebuilder.com #FrontendCon 2019 - @rowdyrabouw 69/157
TabView <TabView height=”100%”> <StackLayout *tabItem=”{title: ‘Rocket Raccoon’}” class=”full rocket”/> <StackLayout *tabItem=”{title: ‘Harley Quinn’}” class=”full harley”/> <StackLayout *tabItem=”{title: ‘Hulk’}” class=”full hulk”/> </TabView> #FrontendCon 2019 - @rowdyrabouw 70/157
TabView .full { background-size: cover; background-position: center; background-repeat: no-repeat; } .rocket { background-image: url(“~/images/rocket-raccoon.jpg”); } .harley { background-image: url(“~/images/harley-quinn.jpg”); } .hulk { background-image: url(“~/images/hulk.jpg”); } #FrontendCon 2019 - @rowdyrabouw 71/157
4 Fantastic Choices
6 Big Heroes
JavaScript
TypeScript
Angular
Vue.js Igor Randjelovic (NativeScript-Vue)
Svelte David Pershouse (Svelte Native)
React Jamie Birch (React NativeScript)
Native Code Objective-C
{N} Angular directory structure app/components/slider/ slider.component.html slider.component.css slider.component.ts slider-routing.module.ts slider-module.ts #FrontendCon 2019 - @rowdyrabouw 85/157
slider.component.html <ActionBar> <NavigationButton visibility=”collapsed”></NavigationButton> </ActionBar> <StackLayout> <Slider></Slider> </StackLayout> #FrontendCon 2019 - @rowdyrabouw 86/157
slider.component.css ActionBar { background-color: #FFFFFF; } StackLayout { padding: 50; } #FrontendCon 2019 - @rowdyrabouw 87/157
slider.component.ts import { Component } from “@angular/core”; @Component({ selector: “app-slider”, moduleId: module.id, templateUrl: “slider.component.html”, styleUrls: [“slider.component.css”] }) export class SliderComponent { constructor() {} } #FrontendCon 2019 - @rowdyrabouw 88/157
slider-routing.module.ts import { NgModule } from “@angular/core”; import { Routes } from “@angular/router”; import { NativeScriptRouterModule } from “nativescript-angular/router”; import { SliderComponent } from “./slider.component”; const routes: Routes = [{ path: “”, component: SliderComponent }]; @NgModule({ imports: [NativeScriptRouterModule.forChild(routes)], exports: [NativeScriptRouterModule] }) export class SliderRoutingModule {} #FrontendCon 2019 - @rowdyrabouw 89/157
slider.module.ts import { NgModule, NO_ERRORS_SCHEMA } from “@angular/core”; import { NativeScriptModule } from “nativescript-angular/nativescript.module”; import { SliderRoutingModule } from “./slider-routing.module”; import { SliderComponent } from “./slider.component”; @NgModule({ imports: [NativeScriptModule, SliderRoutingModule], declarations: [SliderComponent], schemas: [NO_ERRORS_SCHEMA] }) export class SliderModule {} #FrontendCon 2019 - @rowdyrabouw 90/157
#FrontendCon 2019 - @rowdyrabouw 91/157
slider.component.html <ActionBar> <NavigationButton visibility=”collapsed”></NavigationButton> </ActionBar> <StackLayout> <Slider slider-icon></Slider> </StackLayout> • attribute directive • changes the appearance or behavior of an element #FrontendCon 2019 - @rowdyrabouw 92/157
slider.directive.ts import { Directive, ElementRef } from “@angular/core”; import { isIOS } from “platform”; @Directive({ selector: “[slider-icon]” }) export class SliderIconDirective { constructor(private el: ElementRef) { if (isIOS) { const uiSlider = this.el.nativeElement.ios; uiSlider.setThumbImageForState( UIImage.imageNamed(“image.png”), UIControlState.Normal); } } } #FrontendCon 2019 - @rowdyrabouw 93/157
slider.directive.ts import { Directive, ElementRef } from “@angular/core”; import { isIOS } from “platform”; @Directive({ selector: “[slider-icon]” }) export class SliderIconDirective { constructor(private el: ElementRef) { if (isIOS) { const uiSlider = this.el.nativeElement.ios; uiSlider.setThumbImageForState( UIImage.imageNamed(“image.png”), UIControlState.Normal); } } } #FrontendCon 2019 - @rowdyrabouw 94/157
slider.directive.ts import { Directive, ElementRef } from “@angular/core”; import { isIOS } from “platform”; @Directive({ selector: “[slider-icon]” }) export class SliderIconDirective { constructor(private el: ElementRef) { if (isIOS) { const uiSlider = this.el.nativeElement.ios; uiSlider.setThumbImageForState( UIImage.imageNamed(“image.png”), UIControlState.Normal); } } } #FrontendCon 2019 - @rowdyrabouw 95/157
slider.module.ts import { NgModule, NO_ERRORS_SCHEMA } from “@angular/core”; import { NativeScriptModule } from “nativescript-angular/nativescript.module”; import { SliderIconDirective } from “./slider.directive”; import { SliderRoutingModule } from “./slider-routing.module”; import { SliderComponent } from “./slider.component”; @NgModule({ imports: [NativeScriptModule, SliderRoutingModule], declarations: [SliderComponent, SliderIconDirective], schemas: [NO_ERRORS_SCHEMA] }) export class SliderModule {} #FrontendCon 2019 - @rowdyrabouw 96/157
#FrontendCon 2019 - @rowdyrabouw 97/157
slider.component.html <ActionBar> <NavigationButton visibility=”collapsed”></NavigationButton> </ActionBar> <StackLayout> <AbsoluteLayout> <StackLayout #background class=”captain” top=”0” left=”0”></StackLayout> <FlexboxLayout class=”flexcontainer” top=”0” left=”0”> <Slider slider-icon (valueChange)=”onSliderChange($event)”></Slider> </FlexboxLayout> </AbsoluteLayout> </StackLayout> #FrontendCon 2019 - @rowdyrabouw 98/157
slider.component.css .captain { background-image: url(“~/assets/images/captain-america.jpg”); background-repeat: no-repeat; background-position: center; background-size: cover; height: 100%; width: 100%; opacity: 0; } #FrontendCon 2019 - @rowdyrabouw 99/157
slider.component.css .flexcontainer { justify-content: center; align-items: center; height: 100%; width: 100%; } Slider { width: 80%; background-color: #BC1A0F; } #FrontendCon 2019 - @rowdyrabouw 100/157
slider.component.ts import import import import import { { { { { Component, OnInit, ViewChild, ElementRef } from “@angular/core”; Page } from “ui/page”; Slider } from “ui/slider”; StackLayout } from “ui/layouts/stack-layout”; TNSPlayer } from “nativescript-audio”; @Component({ selector: “app-slider”, moduleId: module.id, templateUrl: “slider.component.html”, styleUrls: [“slider.component.css”] }) #FrontendCon 2019 - @rowdyrabouw 101/157
slider.component.ts export class SliderComponent implements OnInit { @ViewChild(“background”) background: ElementRef; private viewStack: StackLayout; private player: TNSPlayer; constructor(private page: Page) {} ngOnInit() { this.page.actionBarHidden = true; this.viewStack = this.background.nativeElement; this.player = new TNSPlayer(); this.player.initFromFile({ audioFile: “~/assets/audio/captain.mp3”, loop: false }); } #FrontendCon 2019 - @rowdyrabouw 102/157
slider.component.ts onSliderValueChange(args) { let slider = <Slider>args.object; // opacity and volume range is 0 - 1 let sliderValue = slider.value / 100; this.viewStack.opacity = sliderValue; if (Math.round(slider.value) > 0) { this.player.play(); this.player.volume = sliderValue; } else { this.player.seekTo(0); this.player.pause(); } } } #FrontendCon 2019 - @rowdyrabouw 103/157
Packages & Libraries
Node Package Manager • commonly known as npm • ready to use JavaScript modules • about 650.000 packages of free, reusable code #FrontendCon 2019 - @rowdyrabouw 106/157
Android Arsenal • libraries for Android (Java / Kotlin) Cocoapods • libraries for iOS (Objective-C / Swift) #FrontendCon 2019 - @rowdyrabouw 107/157
ngx-translate
Multilingual: ngx-translate • internationalization library for Angular 2+ • define translations in different languages • switch between them easily • no hardcoded text/labels, all in one place • start directly, even with one language #FrontendCon 2019 - @rowdyrabouw 110/157
#FrontendCon 2019 - @rowdyrabouw 111/157
Multilingual: ngx-translate { “CHART”: { “BUTTONS”: { “SALARY”: “Salaris / inkomen”, “PENSIONS”: “Pensioenen”, “CHOICES”: “Keuzes voor later”, }, }, } #FrontendCon 2019 - @rowdyrabouw 112/157
Multilingual: ngx-translate { “CHART”: { “BUTTONS”: { “SALARY”: “Salary / income”, “PENSIONS”: “Pensions”, “CHOICES”: “Choices for later”, }, }, } #FrontendCon 2019 - @rowdyrabouw 113/157
Multilingual: ngx-translate <Button [text]=“‘CHART.BUTTONS.SALARY’ | translate” ></Button> <Button [text]=“‘CHART.BUTTONS.PENSIONS’ | translate”></Button> <Button [text]=“‘CHART.BUTTONS.CHOICES’ | translate”></Button> [] = one way data binding in Angular | = display-value transformations #FrontendCon 2019 - @rowdyrabouw 114/157
NativeScript Plugins
What are {N} plugins? When the NativeScript core modules do not provide the native device or platform capability that you need, you can use plugins. • usually for both iOS and Android • JavaScript interface to native platform code #FrontendCon 2019 - @rowdyrabouw 117/157
version-number plugin
version-number plugin » version-number.d.ts » version-number.ios.ts » version-number.android.ts » package.json #FrontendCon 2019 - @rowdyrabouw 119/157
version-number.d.ts export declare class VersionNumber { constructor(); getVersion(): string; } #FrontendCon 2019 - @rowdyrabouw 120/157
version-number.ios.ts declare let NSBundle: any; export class VersionNumber { constructor() {} getVersion(): string { let version = NSBundle.mainBundle.objectForInfoDictionaryKey( “CFBundleShortVersionString” ); return version; } } #FrontendCon 2019 - @rowdyrabouw 121/157
version-number.ios.ts declare let NSBundle: any; export class VersionNumber { constructor() {} getVersion(): string { let version = NSBundle.mainBundle.objectForInfoDictionaryKey( “CFBundleShortVersionString” ); return version; } } #FrontendCon 2019 - @rowdyrabouw 122/157
NSBundle NSView, NSStackView, NSButton, NSImageView, NSSwitch, NSMenu, NSStatusBar, NSPanel, NSWindowTab, NSAlert, NSColorPicker, NSAnimationEffect, NSSpeechRecognizer, NSHapticFeedbackPerformer, … #FrontendCon 2019 - @rowdyrabouw 123/157
NeXTSTEP side step
version-number.android.ts import * as application from “tns-core-modules/application”; declare let android: any; declare let java: any; export class VersionNumber { constructor() {} getVersion(): string { let PackageManager = android.content.pm.PackageManager; let pkg = application.android.context .getPackageManager() .getPackageInfo( application.android.context.getPackageName(), PackageManager.GET_META_DATA ); return java.lang.Integer.toString(pkg.versionCode); } } #FrontendCon 2019 - @rowdyrabouw 130/157
version-number.android.ts import * as application from “tns-core-modules/application”; declare let android: any; declare let java: any; export class VersionNumber { constructor() {} getVersion(): string { let PackageManager = android.content.pm.PackageManager; let pkg = application.android.context .getPackageManager() .getPackageInfo( application.android.context.getPackageName(), PackageManager.GET_META_DATA ); return java.lang.Integer.toString(pkg.versionCode); } } #FrontendCon 2019 - @rowdyrabouw 131/157
AndroidManifest.xml <?xml version=”1.0” encoding=”utf-8”?> <manifest xmlns:android=”http://schemas.android.com/apk/res/android” android:versionCode=”2”> …. </manifest> #FrontendCon 2019 - @rowdyrabouw 132/157
package.json { “name”: “nativescript-version-number”, “version”: “1.0.0”, “main”: “version-number”, “typings”: “version-number.d.ts”, “nativescript”: { “platforms”: { “android”: “6.0.0”, “ios”: “6.0.1” } } …. } #FrontendCon 2019 - @rowdyrabouw 133/157
Install # install from NPM tns plugin add nativescript-version-number # install from local tns plugin add ../nativescript-version-number #FrontendCon 2019 - @rowdyrabouw 134/157
usage import { VersionNumber } from “nativescript-version-number”; const version = new VersionNumber().getVersion(); #FrontendCon 2019 - @rowdyrabouw 135/157
PLUGIN BOILERPLATE
Nativescript Marketplace #FrontendCon 2019 - @rowdyrabouw 140/157
Nativescript Marketplace #FrontendCon 2019 - @rowdyrabouw 141/157
Nativescript Marketplace #FrontendCon 2019 - @rowdyrabouw 142/157
{N} Plugins demo
#FrontendCon 2019 - @rowdyrabouw 144/157
#FrontendCon 2019 - @rowdyrabouw 145/157
#FrontendCon 2019 - @rowdyrabouw 146/157
Philips Hue 147/157
#FrontendCon 2019 - @rowdyrabouw 148/157
#FrontendCon 2019 - @rowdyrabouw 149/157
nativescript-bluetooth import * as bluetooth from “nativescript-bluetooth”; bluetooth.startScanning({ seconds: 4, onDiscovered: peripheral => { if (peripheral.UUID == “CA9F644C-1920-4572-8833-1D137A6T2A05”) { bluetooth.connect({ UUID: peripheral.UUID, onConnected: peripheral => { bluetooth.stopScanning(); // do stuff } }); } } }); #FrontendCon 2019 - @rowdyrabouw 150/157
nativescript-accelerometer import { startAccelerometerUpdates } from “nativescript-accelerometer”; startAccelerometerUpdates(data => { // lean left (0 to -1) / right (0 to 1) let leftOrRight = data.x; // lean forward (0 to -1) / back (0 to 1) let forwardOrBack = data.y; // do stuff }); #FrontendCon 2019 - @rowdyrabouw 151/157
github.com EddyVerbruggen nativescript-pluginshowcase
#FrontendCon 2019 - @rowdyrabouw 153/157
2xr.nl/fc2019 #FrontendCon 2019 - @rowdyrabouw
Dziękuję bardzo! #FrontendCon 2019 - @rowdyrabouw
#FrontendCon 2019 - @rowdyrabouw
Did you know you can use your knowledge of HTML, CSS and JavaScript to build truly native apps for iOS and Android with NativeScript?
I’ll explain what NativeScript is, how it compares to other frameworks and demo how easy and fun it is to get started and how to make use of native capabilities.
Want to see me control a robot or light with just some lines of code? Join me!
The following resources were mentioned during the presentation or are useful additional information.
Here’s what was said about this presentation on social media.
See you at stage A after lunch @FrontEndConnect ! #FrontendCon2019 pic.twitter.com/ewwguiuGyk
— Rowdy Rabouw @ FrontEnd Connect 🇲🇨 (@RowdyRabouw) November 26, 2019
Dear @FrontEndConnect attendees.
— Rowdy Rabouw @ FrontEnd Connect 🇲🇨 (@RowdyRabouw) November 26, 2019
It was a pleasure talking to you about @NativeScript.
Slides are available at https://t.co/mVELagsvt3
If you any questions or remarks find me or send a tweet! #FrontendCon2019
@RowdyRabouw
— FrontEnd Connect (@FrontEndConnect) November 26, 2019
Rowdy Rabouw right before explaining how to unleash our web skills on native! 🤖#FrontendCon2019 pic.twitter.com/nA10dTlnNG
Another slide by @RowdyRabouw on the #native 😱 Thanks!#FrontendCon2019 #FrontendCon #webdevelopment pic.twitter.com/OmBkoLAsqe
— Naturaily (@naturailycom) November 26, 2019
It was a pleasure! Thank you!
— FrontEnd Connect (@FrontEndConnect) November 26, 2019
#MeetTheSpeaker @RowdyRabouw experienced developer working on @vuejs @nodejs & @NativeScript, Developer Expert @ProgressSW Leverage your knowledge of #HTML, #CSS #JavaScript to build truly native apps for 🍏 and 🤖 with NativeScript. Join Rowdy at #FrontendCon2019 pic.twitter.com/sTd3CM0ndG
— FrontEnd Connect (@FrontEndConnect) October 29, 2019