21 Svelte Native

This chapter covers

  • Svelte Native and NativeScript components
  • Developing Svelte Native apps locally
  • NativeScript styling
  • Predefined NativeScript CSS classes
  • The NativeScript UI component library

Svelte Native (https://svelte-native.technology/) enables implementing Android and iOS applications using Svelte by building on top of NativeScript (https:// nativescript.org/). It’s a big topic. The learning path includes how to structure a Svelte Native app, use provided components, use provided layout mechanisms, implement page navigation, use NativeScript-specific styling and themes, use third-party libraries, and integrate with device capabilities. We will only scratch the surface here, but we will cover enough to enable you to start building mobile apps.

NativeScript uses XML syntax (with custom elements), CSS, and JavaScript/ TypeScript to create apps that run on Android and iOS devices. It renders native components rather than web views. NativeScript was created and is maintained by Telerik Corp., which was acquired by Progress Software in 2014.

NativeScript can be used without any web framework. In addition, there are integrations of several popular web frameworks with NativeScript. The NativeScript team supports Angular and Vue integrations, and the community at large supports React and Svelte implementations.

Svelte Native provides a thin layer over the NativeScript API that makes it relatively easy to remain compatible with future NativeScript versions. It was created by David Pershouse, who goes by halfnelson on GitHub and @halfnelson_au on Twitter.

The official Svelte Native tutorial and API documentation can be found on the Svelte Native website: https://svelte-native.technology/.

We will cover all the provided components at a high level. Then we will look at the steps for getting started creating Svelte Native applications. Finally, we will walk through a few example apps, including a basic Hello World app, one that demonstrates nearly all the provided components, and one that uses an add-on library to implement a hamburger (side-drawer) menu.

21.1 Provided components

NativeScript provides many components, and you can build more by combining them. A list of provided components can be found at https://docs.nativescript.org/ ui/overview.

Svelte Native exposes all of these components as DOM elements to Svelte, and they are used as alternatives to the HTML elements used in Svelte apps. They are globally available and so can be used in custom Svelte components without importing them. Documentation on these Svelte components can be found at https://svelte-native .technology/docs.

The names of the provided NativeScript components are camel case and begin with a capital letter. However, the Svelte components that wrap them have names that begin with a lowercase letter. This distinguishes them from custom Svelte components whose names must start with an uppercase letter. For example, instead of <Label text="Hello" /> you would write <label text="Hello" />. We will use the Svelte Native names here when discussing them.

The following sections describe each of the provided components at a high level. Section 21.7 provides examples of using each component.

21.1.1 Display components

The Svelte Native components that display data are listed in table 21.1 after the closest HTML element to which they correspond.

The label component can specify its content using a text attribute, text content, or a formattedString child with span children.

The activityIndicator component displays a platform-specific spinner to indicate that some activity, such as waiting for a response from an API service, is occurring.

Table 21.1 Display components

HTML element

Svelte Native component

<label>

label

<img>

image

<progress>

progress

None

activityIndicator

<ul> or <ol> plus <li>

listView

None

htmlView

None

webView

The listView component displays a scrolling list of items. Its items attribute is set to an array of values that can be any JavaScript type. To specify how each item should be rendered, include a Template child element whose content is components that render the item. To take action when an item is clicked, set on:itemTap to a function.

The htmlView and webView components are used to render a string of HTML. Using webView is preferred because htmlView has a very limited ability to apply CSS styling. The HTML string for an htmlView component is specified with an html attribute. The HTML string for a webView component is specified with a src attribute whose value can be an HTML string, a URL, or a path to an HTML file. Often instances of the webView component must be given a height because that isn’t calculated based on its content. HTML from untrusted sources should be sanitized before it’s passed to a WebView component because it is capable of executing JavaScript code.

21.1.2 Form components

The Svelte Native components that allow user input are listed in table 21.2 after the closest HTML element to which they correspond.

The searchBar component is similar to the textField component, but it adds a magnifying glass icon to the left of the input.

21.1.3 Action components

Components for actions include actionBar, actionItem, and navigationButton.

An actionBar is a toolbar that is displayed at the top of the screen. It typically contains a title and can also contain actionItem and navigationButton components.

Table 21.2 Form components

HTML element

Svelte Native component

<button>

button

<input type="text">

textField (for single-line)

<textarea>

textView (for multiline)

<input type="checkbox">

switch

<input type="radio">

segmentedBar and segmentedBarItem

<select> and <option>

listPicker

<input type="range">

slider

<input type="date">

datePicker

<input type="time">

timePicker

None

searchBar

actionItem components are buttons with platform-specific icons and positioning. An example is the iOS share icon that looks like a square with an arrow pointing up. For Android, the icon is identified by one of the R.drawable constants documented at https://developer.android.com/reference/android/R.drawable. For iOS, the icon is one of the SystemItem constants documented at https://developer.apple.com/documentation/uikit/uibarbuttonitem/systemitem.

21.1.4 Dialog components

Functions that render a dialog include action, alert, confirm, login, and prompt. Table 21.3 describes the contents of each kind of dialog.

Table 21.3 Dialog components

Function name

Dialog contents

action

Message, vertical list of buttons, and a cancel button

alert

Title, message, and a close button

confirm

Title, message, cancel button, and OK button

login

Title, message, username input, password input, cancel button, and OK button

prompt

Title, message, input, cancel button, and OK button

21.1.5 Layout components

Provided components that organize their child components in a certain way are listed in table 21.4 after the closest CSS property value to which they correspond.

Table 21.4 Layout components

CSS property

Svelte Native layout component

display: inline

wrapLayout

display: block

stackLayout

display: flex

flexboxLayout

display: grid

gridLayout

position: absolute

absoluteLayout

None

dockLayout

A page component can optionally contain an actionBar component, and it can only contain one top-level layout component.

absoluteLayout requires its children to specify their absolute location with left and top attributes.

dockLayout positions its children on a given side of the screen: left, right, top, or bottom. The example in listing 21.1 shows how to create the classic layout with a header, footer, and left nav (see figure 21.1). Note that the header and footer children must come before the left child in order for them to span all the way across the display.

Listing 21.1 App that uses dockLayout

<page>
  <dockLayout>
    <label class="header big" dock="top" text="Header" />
    <label class="footer big" dock="bottom" text="Footer" />
    <label class="nav big" dock="left" text="Nav" />
    <stackLayout>
      <label text="Center child #1" />
      <label text="Center child #2" />
    </stackLayout>
  </dockLayout>
</page>
 
<style>
  .big {
    color: white;
    font-size: 24;
    padding: 20;
  }
 
  .footer {
    background-color: purple;
    border-top-width: 3;
  }
 
  .header {
    background-color: red;
    border-bottom-width: 3;
  }
 
  .nav {
    background-color: green;
    border-right-width: 3;
  }
 
  stackLayout {
    background-color: lightblue;
    padding: 20;
  }
</style>

Figure 21.1 A dockLayout example

flexboxLayout implements most of CSS flexbox. Elements that use it can be styled using standard flexbox CSS properties. In addition, it supports the following attributes:

  • justifyContent with values stretch (default), flex-start, flex-end, center, and baseline

  • alignItems with values stretch (default), flex-start, flex-end, center, space-between, and space-around

  • alignContent with the same values as justifyContent only affects containers with more than one line (rarely used)

  • flexDirection with values row (default), column, row-reverse, and column-reverse

  • flexWrap with values nowrap (default), wrap, and wrap-reverse

Child elements of flexboxLayout can specify the attributes alignSelf, flexGrow, flexShrink, flexWrapBefore, and order.

The gridLayout element organizes its children into cells inside rows and columns. A cell can span more than one row and/or column. This is not related to CSS grid layout. The row and column attributes, whose values are comma-separated lists, specify the number of rows and columns, the height of each row, and the width of each column.

There are three options for row and column sizes: an absolute number, auto, or *. Specifying auto makes it as small as possible to accommodate everything in that row or column. Specifying * makes it as large as possible after satisfying the requirements of the other rows or columns. Preceding * with a number acts as a multiplier. For example, columns="100,2*,*" means that the first column is 100 device-independent pixels (DIPs) wide, the second column is 2/3 of the remaining space, and the third column is 1/3 of the remaining space.

Child elements of gridLayout specify their grid position with row and column attributes. They can also specify rowSpan and colSpan attributes to span more than one row and/or column.

stackLayout is the most basic of the supported layouts. It simply lays out its children vertically (default) or horizontally. To lay out children horizontally, add the attribute orientation="horizontal".

wrapLayout is similar to stackLayout, but children wrap to the next row or column when space is exhausted. It differs from stackLayout in that its default orientation is horizontal. To lay out children vertically, add the attribute orientation= "vertical". To specify a fixed width and/or height for each child, add the itemWidth and/or itemHeight attributes.

While not exactly being a layout, the scrollView component does act as a container for other components. It creates a fixed size area where its child components can be scrolled vertically and horizontally.

21.1.6 Navigation components

Components for creating navigation tabs include tabs (displayed at the top of the screen) and bottomNavigation (displayed at the bottom). The tabs component is more full-featured than bottomNavigation. For example, it supports switching tabs by swiping left or right across the content area below the tabs.

Both tabs and bottomNavigation have children that are tabStrip and tabContentItem components. The children of tabStrip components are tabStripItem components, which represent each of the tabs. The tabContentItem components hold the components to render when the corresponding tab is selected. The children of tabContentItem components are typically layout components containing many children.

navigationButton components are buttons for platform-specific functionality, such as the back button.

Another way to support page navigation is to use a side drawer. This is not provided in Native Script. It is an add-on that can be found in the Native Script UI library that is described in section 21.8. See the RadSideDrawer component.

21.2 Getting started with Svelte Native

The easiest way to get started with Svelte Native is to use the online REPL at https:// svelte-native.technology/repl. This is similar to the Svelte REPL. It allows writing, testing, and saving (as GitHub Gists) Svelte Native apps without installing anything. However, unlike the Svelte REPL, it cannot currently download an app, show a list of saved REPL sessions, or recall them.

Another option is to use the online Playground at https://play.nativescript.org/. This has the same options as the Svelte Native REPL, but it also supports projects using any kind of NativeScript project including Angular, Vue, React, Svelte, plain JavaScript, and plain TypeScript.

Both the Svelte REPL and Playground will prompt you to install the NativeScript Playground and NativeScript Preview apps. Do this on all devices to be used for testing apps. For Android, search the Google Play store. For iOS, search the iOS App Store.

To run your app in NativeScript Playground on a device, click the Preview button in the upper right or click QR Code at the top. Both will display a QR code in a dialog. Scan this using the NativeScript Playground app on the devices. It will launch the NativeScript app inside the NativeScript Preview app.

Like the Svelte and Svelte Native REPLs, NativeScript Playground allows you to create an account, log in, and save projects that can be recalled later. To see and select previously saved projects, click Projects in the top nav bar.

After making code changes, click Save at the top. The app will reload on all the devices that have scanned the QR code of the app.

The left side of the Playground includes a file explorer and a palette of components. Component icons can be dragged into code to add an instance of them.

The bottom area has a Devices tab that lists the devices being used. Any number of devices can be tested simultaneously.

21.3 Developing Svelte Native apps locally

For serious development of a Svelte Native app, you will want to create a project where all the required files are available locally and your preferred editor or IDE can be used. To do this, follow these steps:

  1. Install the NativeScript command-line interface (CLI) globally on your computer by entering npm install -g nativescript.

  2. Verify installation by entering tns, which outputs help (“tns” stands for Telerik NativeScript).

  3. Create a new app by entering npx degit halfnelson/svelte-native-template app-name.

  4. Enter cd app-name.

  5. Enter tns preview to build the app and display a QR code.

  6. Open the NativeScript Playground app on a mobile device.

  7. Tap Scan QR Code and point the camera at the QR code. The first time this is done, it will ask you to install the NativeScript Preview app if it is not already installed. Press Install to proceed.

  8. The NativeScript Preview app will launch and display the app. Before making modifications, this will just display a rocket icon followed by the text “Blank Svelte Native App”.

  9. On your computer, modify the app. Changes will live reload on the phone.

Live reload is very fast, taking only about three seconds for a small app.

Another option to consider, not covered here, is the Electron app NativeScript Sidekick. For information on this, see the NativeScript blog: http://mng.bz/04pJ.

Here is a basic Hello World Svelte Native app. Simply modify app/App.svelte to match the following.

Listing 21.2 Hello World Svelte Native app

<script>
  let name = 'World';
</script>
 
<page>
  <actionBar title="Hello World Demo" />
  <stackLayout class="p-20">                    
    <flexboxLayout>
      <label class="name-label" text="Name:" />
      <textField class="name-input" bind:text={name} />
    </flexboxLayout>
    <label class="message" text="Hello, {name}!" />
  </stackLayout>
</page>
 
<style>
  .message {
    color: red;
    font-size: 50;                            
  }
 
  .name-input {
    font-size: 30;
    flex-grow: 1;
  }
 
  .name-label {
    font-size: 30;
    margin-right: 10;
  }
</style>

“p-20” is a provided CSS class that sets padding to 20.

Note that no units are specified on CSS sizes.

Svelte Native apps can be run in an emulator for a specific device. This requires installing a lot of software that is specific to Android and iOS. For details on this, see https://svelte-native.technology/docs#advanced-install.

21.4 NativeScript styling

NativeScript supports a large subset of CSS selectors and properties. It also supports some NativeScript-specific CSS properties. All of these are documented at https:// docs.nativescript.org/ui/styling.

CSS rules that should affect the entire application are placed in app/app.css. By default, this imports ~nativescript-theme-core/css/core.css and ./font-awesome .css (for icons).

Note nativescript-theme-core is deprecated. Svelte Native still uses this by default and it works for now. See https://docs.nativescript.org/ui/theme for details.

Svelte Native component names are treated as custom element names. They can be used as CSS selectors for styling all instances of the component.

For CSS property values that represent a size, when no unit is specified, the value represents device-independent pixels (DIPs). This is the recommended unit to use. A “px” suffix can be added to size values for plain pixels that vary across devices, but using this unit is discouraged. Percentages with a “%” suffix are also supported.

Other supported CSS features include

  • CSS variables

  • The calc function

  • The generic fonts serif, sans-serif, and monospace

  • Custom fonts in TTF or OTF format found in the app/fonts directory

  • Importing a CSS file from another with @import url('file-path')

  • Using Sass (with some setup)

Some CSS shorthand properties are not supported. For example, instead of border: solid red 3px;, use border-color: red; border-width: 3px;. The border-style property is not supported. All borders are solid.

The outline shorthand property and all CSS properties whose names begin with outline- are not supported.

To change the style of text within a block of text, use the formattedString element with span elements as children. Each span can specify different styling. The formattedString element must be a child of a component that supports text content such as button, label, textField, and textView. The following listing shows an example of using formattedString.

Listing 21.3 formattedString example

<page>
  <stackLayout class="p-20">
    <label class="panagram" textWrap="true">             
      <formattedString>
        <span class="fox">The quick brown fox</span>     
        <span text=" jumps over " />                     
        <span class="dog">the lazy dog</span>            
        <span text="." />
      </formattedString>
    </label>
  </stackLayout>
</page>
 
<style>
  .panagram {
    font-size: 30;
  }
 
  .fox {
    color: red;
    font-weight: bold;
  }
 
  .dog {
    color: blue;
    font-style: italic;
  }
</style>

A panagram is a sentence that contains all the letters of the alphabet.

This text will be red and bold.

This text will be black and normal.

This text will be blue and italic.

21.5 Predefined NativeScript CSS classes

Components can be styled by defining CSS classes that match their name. For example, the following makes all label components blue and bold:

label {
  color: blue;
  font-weight: bold;
}

NativeScript provides a set of predefined CSS classes that can be applied to NativeScript components for common styling needs. These are conveniences that can be used in place of explicitly applying CSS properties.

These predefined classes are appropriate for applying to label components:

  • Headings --h1, h2, h3, h4, h5, and h6

  • Paragraphs --body for medium-sized text

  • Footnotes --footnote for small text

  • Alignment --text-left, text-center, and text-right

  • Case --text-lowercase, text-uppercase, and text-capitalize

  • Weight --font-weight-normal and font-weight-bold

  • Style --font-italic

The following snippet is an example of using a predefined class.

<label class="h1" text="This is BIG!" />

There are many provided CSS classes that specify the desired padding and margin (see table 21.5). In these class names, # is a placeholder for a number, which must be one of 0, 2, 5, 10, 15, 20, 25, or 30.

Table 21.5 CSS classes that specify padding and margin

Sides

Padding

Margin

All

p-#

m-#

Top (t)

p-t-#

m-t-#

Right (r)

p-r-#

m-r-#

Bottom (b)

p-b-#

m-b-#

Left (l)

p-l-#

m-l-#

Left and right

p-x-#

m-x-#

Top and bottom

p-y-#

m-y-#

There are several provided CSS classes for styling forms and form elements such as buttons. For details, see the NativeScript documentation for forms (https://docs .nativescript.org/ui/theme#forms) and for buttons (https://docs.nativescript.org/ui/ theme#buttons).

The equivalent of the HTML horizontal rule <hr> element is to use an empty stackLayout with the hr class, as follows:

<stackLayout class="hr" />

To overlay the default horizontal rule with a thicker, colored rule, add CSS properties to the hr CSS class as follows:

.hr {
  --size: 10;
  height: var(--size);
    border-color: green;
    border-width: calc(var(--size) / 2);
  }

For more details on NativeScript styling, see https://docs.nativescript.org/ui/styling.

21.6 NativeScript themes

NativeScript supports styling themes through a large set of CSS classes. To install a default theme along with 11 more themes, enter npm install @nativescript/theme. Additional themes not provided by NativeScript can also be installed. Each theme simply overrides the predefined CSS classes. These support light and dark modes.

To use the default theme, edit app/app.css and replace the import of core.css with the following:

@import '~@nativescript/theme/css/core.css';
@import '~@nativescript/theme/css/default.css';

To use one of the other included themes, replace default in the second line above with one of aqua, blue, brown, forest, grey, lemon, lime, orange, purple, ruby, or sky.

21.7 Comprehensive example

Up until now, we’ve been using the Travel Packing app as our example, but we’re going to switch gears here in the interest of brevity. The Svelte Native app in this section provides examples of using nearly all of the components included in NativeScript. It provides a good jumping off point for implementing your own Svelte Native applications.

The app has three pages, each presenting examples of components in a given category:

  • The first category is components that display information. This includes the label, webView, image, and progress components.

  • The second category is components that accept user input. This includes textField, textView, switch, segmentedBar, datePicker, timePicker, listView, listPicker, and slider.

  • The third category is functions that render dialogs and components used for search queries. This includes the functions login, prompt, action, and confirm. It also includes the searchBar and activityIndicator components.

Page navigation is configured in App.svelte (see figure 21.2).

Figure 21.2 Component that handles page navigation

Listing 21.4 Component that handles page navigation in app/App.svelte

<script>
  import DisplayComponents from './DisplayComponents.svelte';    
  import InputComponents from './InputComponents.svelte';
  import OtherComponents from './OtherComponents.svelte';
 
  function onTapDelete() {
    console.log('App.svelte onTapDelete: entered');
  }
 
  function onTapShare() {
    console.log('App.svelte onTapShare: entered');
  }
</script>
 
<page>
  <actionBar title="Svelte Native Demo">                         
    <!-- button with upload icon -->
    <actionItem on:tap="{onTapShare}"
      ios.systemIcon="9" ios.position="left"
      android.systemIcon="ic_menu_share" android.position="actionBar" />
    <!-- button with trash can icon -->
    <actionItem on:tap="{onTapDelete}"
      ios.systemIcon="16" ios.position="right"
      android.systemIcon="ic_menu_delete"
      text="delete" android.position="popup" />
  </actionBar>
 
  <tabs>                                                         
    <tabStrip>
      <tabStripItem>
        <label text="Display" />
        <image src="font://&#xF26C;" class="fas" />              
      </tabStripItem>
      <tabStripItem>
        <label text="Input" />
        <image src="font://&#xF11C;" class="far" />              
      </tabStripItem>
      <tabStripItem>
        <label text="Other" />
        <image src="font://&#xF002;" class="fas" />              
      </tabStripItem>
    </tabStrip>
 
    <tabContentItem>                                             
      <DisplayComponents />
    </tabContentItem>
 
    <tabContentItem>
      <InputComponents />
    </tabContentItem>
 
    <tabContentItem>
      <OtherComponents />
    </tabContentItem>
  </tabs>
</page>
 
<style>
  tabStrip {
    --tsi-unselected-color: purple;
    --tsi-selected-color: green;
 
    background-color: lightblue;
    highlight-color: green;
  }
 
  tabStripItem {
    color: var(--tsi-unselected-color); /* for icon */
  }
 
  tabStripItem > label {
    color: var(--tsi-unselected-color); /* for text */
  }
 
  tabStripItem:active {
    color: var(--tsi-selected-color); /* for icon */
  }
 
  tabStripItem:active > label {
    color: var(--tsi-selected-color); /* for text */
  }
</style>

These three imports are for the “page” components.

The actionBar component is rendered at the top of the screen.

The tabs component is rendered below the actionBar. It contains one tab for each page of the app.

This is a monitor icon.

This is a keyboard icon.

This is a magnifying glass icon.

There is one tabContentItem for each tabStripItem. Each of these render a page of the app.

Note The image elements in listing 21.4, such as the monitor, keyboard, and magnifying glass icons, use FontAwesome icons. To find the hex code for a given icon, search for them at https://fontawesome.com/icons. For example, searching for “house” and clicking the “home” icon shows that its code is f015 and its class is fas. The CSS classes fab, far, and fas are defined in app/fontawesome.css. The CSS class fa-home is not defined by default in Svelte Native.

The following app-wide CSS styling is added to the existing app.css file.

Listing 21.5 Global styling in app/app.css

button {
  background-color: lightgray;
  border-color: darkgray;
  border-width: 3;
  border-radius: 10;
  font-weight: bold;
  horizontal-align: center; /* to size according to content */
  padding: 10;
}
 
label {
  color: blue;
  font-size: 18;
  font-weight: bold;
}
 
.plain {
  color: black;
  font-size: 12;
  font-weight: normal;
}
 
.title {
  border-top-color: purple;
  border-top-width: 5px;
  color: purple;
  font-size: 20;
  font-weight: bold;
  font-style: italic;
  margin-top: 10;
  padding-top: 10;
}

This app uses several Svelte stores in order to share data across components. They are all defined in stores.js.

Listing 21.6 Stores in app/stores.js

import {derived, writable} from 'svelte/store';
 
export const authenticated = writable(false);
export const backgroundColor = writable('pink');        
export const favoriteColorIndex = writable(0);
export const firstName = writable('');
 
// Not a store.
export const colors = ['red', 'orange', 'yellow', 'green', 'blue', 'purple'];

async function evaluateColor(color) {                   
  if (color === 'yellow') {
    alert({
      title: 'Hey there!',
      message: 'That is my favorite color too!',
      okButtonText: 'Cool'
    });
  } else if (color === 'green') {
    const confirmed = await confirm({
      title: 'Confirm Color',
      message: 'Are you sure you like that color?',
      okButtonText: 'Yes',
      cancelButtonText: 'No'
    });

    if (!confirmed) favoriteColorIndex.set(0);          
  }
}

export const favoriteColor = derived(                   
   favoriteColorIndex,
  index => {
    const color = colors[index];
    evaluateColor(color);
    return color;
  }
);

This is used as the background color of all the pages. It is pink when the user has not logged in. It is light green when they have.

This function is called by the favoriteColor derived store defined below.

This sets the favorite color to “red” if the user doesn’t like “green”.

This is a derived store based on the favoriteColorIndex store. It updates its value to a color name every time the value of the favoriteColorIndex store changes.

The page defined by the DisplayComponents component provides examples of components that display information (see figure 21.3).

Figure 21.3 DisplayComponents component

htmlView and webView issues

There are some issues to consider when using the htmlView and webView components.

The NativeScript docs say “The HtmlView component has limited styling capabilities. For more complex scenarios use the WebView component.” Using htmlView is fine for simple needs such as rendering rich text using bold and italics. For more advanced styling, use webView.

The webView component does not calculate its height based on its content, so it needs to be given a height in CSS unless the layout being used gives it a height.

The Svelte Native docs show that the src attribute of a webView can be set to a path to an HTML file. Webpack must be properly configured to include the HTML file in the bundle. For tips on doing this, see the issue at https://github.com/halfnelson/svelte-native/issues/138.

Listing 21.7 DisplayComponents component in app/DisplayComponents.svelte

<script>
  import {backgroundColor} from './stores';

  const myHtml = `                                                         
    <div>
      <span style="color: red">The quick brown fox</span>
      <span>jumps over</span>
      <span style="color: blue">the lazy dog</span>
    </div>
  `;

  let progressPercent = 0;

  function startProgress() {                                               
    progressPercent = 0;
    const token = setInterval(() => {
      progressPercent++;
      if (progressPercent === 100) clearInterval(token);
    }, 10);
  }
</script>

<scrollView>
  <stackLayout
    backgroundColor={$backgroundColor}
    class="p-20"
  >
    <label class="title" text="label" />
    <label class="plain" text="some text" />

    <label class="title" text="label with formattedString" />
    <label class="panagram" textWrap="true">
      <formattedString>                                                    
        <span class="fox">The quick brown fox</span>
        <span text=" jumps over " />
        <span class="dog">the lazy dog</span>
        <span text="." />
      </formattedString>
    </label>

    <label class="title" text="image" />
    <wrapLayout class="image-frame">                                       
      <image src="~/svelte-native-logos.png" stretch="aspectFit" />        
    </wrapLayout>

    <label class="title" text="webView" />
    <webView src="<h1>I am a webView.</h1>" />
    <webView src={myHtml} />
    <webView style="height: 300" src="https://svelte-native.technology/" />

    <label class="title" text="progress" />
    <button on:tap={startProgress}>Start Progress</button>
    <progress class="progress" maxValue={100} value="{progressPercent}" />
  </stackLayout>
</scrollView>

<style>
  .dog {
    color: blue;
    font-style: italic;
  }

  .fox {
    color: red;
    font-weight: bold;
  }

  .image-frame {
    background-color: white;
    padding: 10;
  }

  .panagram {
    background-color: linen;
    color: black;
    font-size: 26;
    font-weight: normal;
    margin-bottom: 20;
    padding: 20;
  }

  progress {
    color: red;
    margin-bottom: 10;
    scale-y: 5; /* changes height */
  }

  webView {
    border-color: red;
    border-width: 1;
    height: 50;                                                            
  }
</style>

This is displayed in a webView below.

Tapping the “Start Progress” button causes this to be called. It animates the value of the progress bar.

This renders a string where runs of text can have different styling.

The CSS padding property doesn’t affect image components. This is a workaround to add padding to an image.

Svelte Native treats ~ at the beginning of a file path as an alias to the app directory. This image can be found at https://github.com/mvolkmann/svelte-native-components/blob/master/app/svelte-native-logos.png.

The rendered page can be scrolled inside the webView component.

No webView content will be visible without this.

The page defined by the InputComponents component provides examples of components that gather user input (see figure 21.4).

Figure 21.4 InputComponents component

Listing 21.8 InputComponents component in app/InputComponents.svelte

<script>
  import {Template} from 'svelte-native/components'
 
  import {
    backgroundColor,
    colors,
    favoriteColor,
    favoriteColorIndex,
    firstName
  } from './stores';
 
  const temperatures = ['Cold', 'Warm', 'Hot'];
 
  let birthday = new Date(1961, 3, 16);
  let likeRunning = false;
  let reason = '';
  let stars = 3;
  let temperatureIndex = 1;
 
  let quittingTime = new Date();
  quittingTime.setHours(17);
  quittingTime.setMinutes(0);
  quittingTime.setSeconds(0);
 
  function formatDate(date) {                               
    if (!date) return '';
    let month = date.toLocaleDateString('default', {month: 'long'});
    const index = month.indexOf(' ');
    if (index) month = month.substring(0, index);
    return `${month} ${date.getDate()}, ${date.getFullYear()}`;
  }
 
  function formatTime(date) {                               
    if (!date) return '';
 
    let hours = date.getHours();
    const amPm = hours < 12 ? 'AM' : 'PM';
    if (hours >= 12) hours -= 12;
 
    let minutes = date.getMinutes();
    if (minutes < 10) minutes = '0' + minutes;
 
    return `${hours}:${minutes} ${amPm}`;
  }
 
  function onFavoriteColor(event) {
    $favoriteColorIndex = event.value; // odd that this is an index
  }
 
  function onTapColor(event) {
    $favoriteColorIndex = event.index;
  }
 
  function starChange(event) {
    stars = Math.round(event.value);                         
  }
</script>
 
<scrollView>
  <stackLayout
    backgroundColor={$backgroundColor}
    class="p-20"
  >
    <!-- like HTML <input type="text"> -->
    <label class="title" text="textField" />
    <wrapLayout>
      <label text="First Name" />
      <textField class="first-name" hint="first name" bind:text={$firstName} />
      <label class="plain" text="Your first name is {$firstName}." />
    </wrapLayout>
 
    <!-- like HTML <textarea> -->
    <label class="title" text="textView" />
    <wrapLayout>
      <label text="What would you say you do here?" />
      <textView class="reason" hint="reason for being" bind:text={reason} />
      <label class="plain" text="Your reason for being is {reason}." textWrap="true" />
    </wrapLayout>
 
    <!-- like HTML <input type="checkbox"> -->
    <label class="title" text="switch" />
    <wrapLayout>
      <label text="Like Running?" />
      <switch bind:checked={likeRunning} />
      <label
        class="plain"
        text="You{likeRunning ? '' : ' do not'} like running."
      />
    </wrapLayout>
 
    <!-- like HTML <input type="radio"> -->
    <label class="title" text="segmentedBar" />
    <segmentedBar
      bind:selectedIndex={temperatureIndex}
      selectedBackgroundColor="yellow"
    >
      <segmentedBarItem title="Cold" />
      <segmentedBarItem title="Warm" />
      <segmentedBarItem title="Hot" />
    </segmentedBar>
    <segmentedBar                                             
      bind:selectedIndex={temperatureIndex}
      selectedBackgroundColor="yellow"
    >
      {#each temperatures as temp}
        <segmentedBarItem title={temp} />
      {/each}
    </segmentedBar>
    <label
      class="plain"
      text="You are feeling {temperatures[temperatureIndex]}."
    />
 
    <!-- like HTML <input type="date"> -->
    <label class="title" text="datePicker" />
    <wrapLayout>
      <label text="Birthday" />
      <datePicker bind:date={birthday} />
      <label class="plain" text="You selected {formatDate(birthday)}." />
    </wrapLayout>
 
    <!-- like HTML <input type="time"> -->
    <label class="title" text="timePicker" />
    <wrapLayout>
      <label text="Quitting Time" />
      <timePicker bind:time={quittingTime} />
      <label class="plain" text="You will quit at {formatTime(quittingTime)}." />
    </wrapLayout>
 
    <!-- like HTML <ul>, <ol>, or <select> -->
    <label class="title" text="listView" />
    <wrapLayout>
      <listView items={colors} on:itemTap={onTapColor}>      
        <Template let:item={color}>
          <label
            class="list"
            class:selected={color === $favoriteColor}        
            text="One of the colors is {color}."
          />
        </Template>
      </listView>
    </wrapLayout>
    <label class="plain" text="You selected {$favoriteColor}." />
 
    <!-- like HTML <select> -->
    <label class="title" text="listPicker" />
    <wrapLayout>
      <label text="Favorite Color" />
      <listPicker
        items={colors}
        selectedIndex={$favoriteColorIndex}
        on:selectedIndexChange={onFavoriteColor}
      />
      <label class="plain" text="You selected {$favoriteColor}." />
    </wrapLayout>
 
    <!-- like HTML <input type="range"> -->
    <label class="title" text="slider" />
    <wrapLayout>
      <label text="Stars" />
      <slider
        minValue={1}
        maxValue={5}
        value={stars}                                    
        on:valueChange={starChange}
      />
      <label class="plain" text="You rated it {stars} stars" />
    </wrapLayout>
  </stackLayout>
</scrollView>
 
<style>
  .first-name {
    width: 100%;
  }
 
  .list {
    border-color: blue;
    border-width: 1;
    margin: 0;
  }
 
  .reason {
    width: 100%;
  }
 
  segmentedBar {
    color: red;
    margin-bottom: 10;
    margin-top: 10;
  }
 
  .selected {
    background-color: lightgreen;
  }
</style>

This formats a Date object as a string like “April 16, 1961”.

This formats a Date object as a string like “5:00 PM”.

This is an attempt to get the “stars” slider to snap to an integer value, but it does not work.

This shows another way to provide items to the segmentedBar component. The API docs show passing an array of items using the “items” prop, but that does not work. The children must be segmentedBarItem components.

This acts like an HTML select element when there is an on:itemTap handler.

This is not reevaluated when the value of the favoriteColor store changes, so the presence of the selected CSS class is not updated.

The slider position doesn’t change when the starChange function changes the value of stars. See the issue at https://github.com/halfnelson/svelte-native/issues/128.

The page defined by the OtherComponents component provides examples of components that render a dialog or prompt for a search query (see figure 21.5).


Figure 21.5 OtherComponents component

Figure 21.6 Login dialog

When the Login button is tapped, the dialog in figure 21.6 is displayed.

If the user enters a correct username and password, the background of all the pages changes from pink to light green to indicate that they have been authenticated (see figure 21.7).


Figure 21.7 After successful login

Figure 21.8 Prompt for first name

When the Prompt for First Name button is tapped, the dialog in figure 21.8 is displayed.

When the Pick a Color button is tapped, the dialog in figure 21.9 is displayed.


Figure 21.9 Prompt for color

Figure 21.10 Search results

After entering part of a color name in the search input, a dialog like the one in figure 21.10 is displayed.

Listing 21.9 shows the code for OtherComponents that implements the preceding functionality.

Listing 21.9 OtherComponents component in app/OtherComponents.svelte

<script>
  import {login} from 'tns-core-modules/ui/dialogs'
  import {
    authenticated,
    backgroundColor,
    colors,
    favoriteColor,
    favoriteColorIndex,
    firstName
  } from './stores';
 
  let busy = false;
  let query = '';
 
  // Should this be handled in stores.js?
  $: $backgroundColor = $authenticated ? 'lightgreen' : 'pink';
 
  async function getFirstName() {
    const res = await prompt({
      title: 'First Name',
      message: 'Please tell me your first name.',
      okButtonText: 'Here it is',
      cancelButtonText: 'I will not share that'
    });
    if (res.result) $firstName = res.text;
  }
 
  function onSearchSubmit() {
    busy = true;
    setTimeout(async () => { // to demonstrate activityIndicator component
      // The event object passed to this does not contain the search text
      // or any interesting properties.
      const q = query.toLowerCase();
      const matches = colors.filter(color => color.includes(q));
      busy = false;
      await alert({
        title: 'Color Matches',
        message: matches.length ? matches.join(', ') : 'no matches',
        okButtonText: 'Close'
      });
      query = ''; // reset
    }, 1000);
  }
 
  async function pickColor() {
    const NONE = 'No Thanks';
    const choice = await action('Pick a color', NONE, colors);
    if (choice !== NONE) {
      $favoriteColorIndex = colors.findIndex(c => c === choice);
    }
  }
 
  async function promptForLogin() {
    const res = await login({
      title: 'Please Sign In',
      message: 'This will unlock more features.',
      userNameHint: 'username',
      passwordHint: 'password',
      okButtonText: 'OK',
      cancelButtonText: 'Cancel',
    });
    if (res.result) {
      // Authenticate the user here.
      $authenticated = res.userName === 'foo' && res.password === 'bar';
      if (!$authenticated) {
        alert({
          title: 'Login Failed',
          message: 'Your username or password was incorrect.',
          okButtonText: 'Bummer'
        });
      }
    }
  }
</script>
 
<scrollView>
  <stackLayout
    backgroundColor={$backgroundColor}
    class="p-20"
  >
    <label class="title" text="login" />
    <wrapLayout>
      {#if $authenticated}
        <button on:tap={() => $authenticated = false}>Logout</button>
      {:else}
        <button on:tap={promptForLogin}>Login ...</button>
      {/if}
    </wrapLayout>
 
    <label class="title" text="prompt" />
    <button on:tap={getFirstName}>Prompt for First Name</button>
    <label class="plain" text="Your first name is {$firstName}." />
 
    <label class="title" text="action" />
    <button on:tap={pickColor}>Pick a Color</button>
    <label class="plain" text="You selected {$favoriteColor}." />
 
    <label class="title" text="searchBar" />
    <!-- Using gridLayout to position activityIndicator over searchBar. -->
    <gridLayout rows="*">
      <searchBar
        hint="Enter part of a color name."
        bind:text={query}
        on:submit={onSearchSubmit}
        row="0"
      />
      <!-- The activityIndicator height and width attributes
        control the allocated space.
        But the size of the spinner cannot be changed,
        only the color. -->
      <activityIndicator busy={busy} row="0"/>
    </gridLayout>
  </stackLayout>
</scrollView>
 
<style>
  activityIndicator {
    color: blue;
  }
 
  searchBar {
    margin-bottom: 10;
  }
</style>

21.8 NativeScript UI component library

NativeScript UI is a collection of NativeScript components that are not included by default. They include RadAutoCompleteTextView, RadCalendar, RadChart, RadDataForm, RadGauge, RadListView, and RadSideDrawer.

To use these components in a Svelte Native app, first create the app using npx degit halfnelson/svelte-native-template app-name. Then see the instructions on the NativeScript UI GitHub page at https://github.com/halfnelson/svelte-native-nativescript-ui.

RadListView is a particularly popular component. It renders a list of items with animations and support for many gestures including “pull to refresh” and “swipe actions.”

Another popular component is RadSideDrawer. This provides a drawer-based side nav with a hamburger menu that is used for page navigation. Let’s walk through an example that uses this.

To prepare a Svelte Native app for using a RadSideDrawer component, do the following:

  1. Enter npm install svelte-native-nativescript-ui.

  2. Enter tns plugin add nativescript-ui-sidedrawer.

Our app will have two pages named “About” and “Hello”. The About page merely describes the app. The Hello page allows the user to enter a name, and it renders a greeting message.

The side drawer contains the page names. To display the side drawer, click the hamburger icon in the upper left of a page, or drag right from the left edge of the screen. Tap a page name to close the side drawer and navigate to that page. Tap outside of the side drawer to close it without navigating to a different page.

Figures 21.11, 21.12, and 21.13 show the relevant parts of the two pages and the side drawer.

Figure 21.11 About page

Figure 21.12 Hello page

Figure 21.13 Side drawer open

Configuration of the side drawer is specified in App.svelte.

Listing 21.10 Topmost component in app/App.svelte

<script>
  import {onMount} from 'svelte';
  import RadSideDrawerElement from 
    'svelte-native-nativescript-ui/sidedrawer';
  import AboutPage from './AboutPage.svelte';
  import HelloPage from './HelloPage.svelte';
  import {goToPage, setDrawer} from './nav';
 
  RadSideDrawerElement.register();                               
 
  let drawer;
 
  onMount(() => setDrawer(drawer));                              
</script>
 
<page>
  <radSideDrawer bind:this={drawer} drawerContentSize="200">     
    <radSideDrawer.drawerContent>                                
      <stackLayout>
        <label                                                   
          class="fas h2"
          text="&#xF00D;"
          padding="10"
          horizontalAlignment="right"
          on:tap={() => drawer.closeDrawer()}
        />
        <label text="About" on:tap={() => goToPage(AboutPage)} />
        <label text="Hello" on:tap={() => goToPage(HelloPage)} />
      </stackLayout>
    </radSideDrawer.drawerContent>
    <radSideDrawer.mainContent>
      <frame id="mainFrame" defaultPage={HelloPage} />           
    </radSideDrawer.mainContent>
  </radSideDrawer>
</page>
 
<style>
  label {
    color: white;
    font-size: 30;
    padding-bottom: 30;
  }
 
  stackLayout {
    background-color: cornflowerblue;
    padding: 20;
  }
</style>

This is required to use the radSideDrawer element.

This lets nav.js know about the drawer.

The default drawer width is wider than this app needs, so we set it to 200.

This defines the content of the drawer.

This renders an “X” that can be clicked to close the drawer.

This is where each of the pages will be rendered.

Functions for navigating to a given page (optionally passing it props) and toggling the drawer between being open and closed are defined in nav.js. The drawer is passed to setDrawer in the preceding listing for App.svelte.

Listing 21.11 Navigation functions in app/nav.js

import {navigate} from 'svelte-native';
 
let drawer;
 
export function setDrawer(d) {                                             
  drawer = d;
}
 
export function goToPage(page, props) {                                    
  drawer.closeDrawer();                                                    
  // Setting clearHistory to true prevents "<Back" button from appearing.
  navigate({page, props, clearHistory: true, frame: 'mainFrame'});         
}
 
export function toggleDrawer() {                                           
  drawer.toggleDrawerState();
}

This saves the drawer so it can be used in the goToPage and toggleDrawer functions.

This is currently only called when a page name in the drawer is tapped. However, it could be used from any page to navigate to another page.

“mainFrame” is the ID of the frame created in App.svelte.

This is called when the hamburger icon in the Header component is tapped.

Each page renders a Header component. It contains an actionBar that renders a hamburger icon and a page title. Tapping the hamburger icon opens the side drawer.

Listing 21.12 Header component in app/Header.svelte

<script>
  import {toggleDrawer} from './nav'
  import {isAndroid} from "tns-core-modules/platform"    
 
  export let title = '';
</script>
 
<actionBar title={title}>
  {#if isAndroid}                                        
    <navigationButton icon="res://menu" on:tap={toggleDrawer} />
  {:else}
    <actionItem icon="res://menu" ios.position="left" on:tap={toggleDrawer} />
  {/if}
</actionBar>
 
<style>
  actionBar {
    background-color: cornflowerblue;
    color: white;
  }
</style>

This holds a Boolean value that indicates whether the current device is running Android.

We want to display the hamburger menu in a platform-specific way.

The AboutPage component used in listing 21.10 is shown in the following listing.

Listing 21.13 AboutPage component in app/AboutPage.svelte

<script>
  import Header from './Header.svelte';
  import {singleLine} from './util';       
 
  let description = singleLine`
    This is a Svelte Native app that demonstrates
    using side drawer navigation.
  `;
</script>
 
<page>
  <Header title="About" />
  <stackLayout class="p-20">
    <label text={description} textWrap="true">
    </label>
  </stackLayout>
</page>
 
<style>
  label {
    color: red;
    font-size: 32;
  }
</style>

This function name can appear at the beginning of a tagged template literal to turn a multiline, indented string into a single-line string with no indentation.

The HelloPage component used in listing 21.10 is shown in the following listing.

Listing 21.14 HelloPage component in app/HelloPage.svelte

<script>
  import Header from './Header.svelte';
 
  let name = 'World';
</script>
 
<page>
  <Header title="Hello" />
  <stackLayout class="p-20">
    <flexboxLayout>
      <label text="Name" />
      <textField bind:text={name} />     
    </flexboxLayout>
 
    <label class="greeting" text="Hello, {name}!" textWrap="true" />
  </stackLayout>
</page>
 
<style>
  .greeting {
    color: blue;
    font-size: 40;
  }
 
  label {
    font-size: 20;
    font-weight: bold;
  }
 
  textField {
    flex-grow: 1;
    font-size: 20;
  }
</style>

The user can enter a name here.

The singleLine function used in listing 21.13 is shown in the following listing.

Listing 21.15 singleLine function in app/util.js

// This is a tagged template literal function that
// replaces newline characters with a space
// and then replaces consecutive spaces with one.
export function singleLine(literals) {
  return literals
    .join(' ')
    .replace(/
/g, ' ')
    .replace(/  +/g, ' ')
    .trim();
}

That’s it! Copy the techniques from this code into any Svelte Native apps where you want to use drawer-based page navigation.

21.9 Svelte Native issues

Svelte Native is a work in progress, and there are still some rough edges.

Some code errors cause the NativeScript Preview app to crash. This requires making code corrections, rerunning the tns command, returning to the NativeScript Playground mobile app, and rescanning the QR code. Sometimes after making a valid code change, the app will crash again, and a long stack trace with no references to your code will be output. If there really are no errors in your code, repeating these steps often results in the app running correctly.

You have almost reached the end of your journey, but keep reading. There is plenty of good material in the appendixes!

Summary

  • Svelte Native builds on NativeScript to enable developing mobile applications using Svelte.

  • NativeScript provides a set of predefined components.

  • You can get started learning Svelte Native online using the Svelte Native REPL or the NativeScript Playground. Neither of these options requires installing any software.

  • For more serious Svelte Native development, you will want to create a project locally and use your preferred editor or IDE for development.

  • The NativeScript UI component library provides more advanced components than those provided by NativeScript.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.133.12.172