Updated 2019-8-1: updated for Glimmer-Native 0.0.5
NativeScript is a platform for building truly native mobile apps using a variety of different web frameworks. Up until now Angular and Vue.js have been your options–but now the Glimmer-Native project gives you the option to build native apps with Glimmer.js as well! Glimmer-Native is currently in alpha, and input from the community is welcomed to help it get built out.
Let’s give Glimmer-Native a try building the traditional todo list app!
NativeScript works on both iOS and Android. If you’d like to preview your app on iOS, you’ll need to have a Mac with Xcode installed. If you’d like to preview your app on Android, you’ll need to install Android Studio and set up an Android virtual device.
To begin, install Ember CLI and the NativeScript CLI if you don’t already have both installed:
$ npm install -g ember
$ npm install -g nativescript
Next, create a new Ember project with the glimmer-native-blueprint
– this will set up your project to be a NativeScript app instead of a web app:
$ ember new todo-glimmer-native -b glimmer-native-blueprint
$ cd todo-glimmer-native
Let’s go ahead and run the app to confirm it works. Run one of the following two commands, depending on if you want to run on iOS or Android:
$ tns run ios --bundle
or
$ tns run android --bundle
After a minute or two, your app should appear on the simulated device saying “Welcome to Glimmer Native”. Sweet!
Your app’s components live under src/ui/components/
. Currently there’s just one component, TodoGlimmerNative
. Open the TodoGlimmerNative/
directory, and the component.ts
file inside it. You’ll see:
import Component from '@glimmer/component';
export default class TodoGlimmerNative extends Component {
title = "Welcome to Glimmer Native"
}
So far this looks just like a normal Glimmer.js component class. Let’s check out the template.hbs
:
The template is where the main difference is between Glimmer.js web apps and Glimmer-Native apps. Instead of rendering HTML elements, we “render” NativeScript components that, in turn, control your native app. You can read a list of supported Glimmer-Native components in Glimmer-Native’s readme.
We can see that the title shown in our action bar comes from the title
property of the component. As our first change, let’s change that title. Make this change in TodoGlimmerNative/component.ts
:
export default class TodoGlimmerNative extends Component {
- title = "Welcome to Glimmer Native"
+ title = 'Todos'
}
When you save the file, your native app should reload, and the title should now be “Todos”.
Next, let’s start to add the core data for our app: the list of todos. We’ll define some starting data in TodoGlimmerNative/component.ts
:
export default class TodoGlimmerNative extends Component {
title = "Welcome to Glimmer Native"
+ todos = [
+ { id: 1, label: 'Buy bread' },
+ { id: 2, label: 'Buy milk' },
+ { id: 3, label: 'Buy eggs' },
+ ]
}
Let’s render that out on the screen, in TodoGlimmerNative/template.hbs
:
<Page>
<ActionBar>
<Label text={{this.title}} class="label" />
</ActionBar>
+ <StackLayout>
+ {{#each this.todos key="id" as |todo|}}
+ <Label
+ text={{todo.label}}
+ />
+ {{/each}}
+ </StackLayout>
</Page>
StackLayout
is one of the NativeScript layout containers available to our app. It simply stacks elements one after the other. We use a normal Glimmer/Handlebars #each
helper to loop through our labels and output them. Note that we pass a key
argument to the #each
helper to help Glimmer track which item in the list is which.
Save the file and your todo items should appear in the app.
Next, let’s add the ability to add new todos to the list. Add a few form elements to TodoGlimmerNative/template.hbs
:
<StackLayout>
+ <TextField
+ hint="New Todo"
+ text={{this.newTodoLabel}}
+ {{on "textChange" (action handleChangeText)}}
+ />
+ <Button
+ text="Add Todo"
+ {{on "tap" (action addTodo)}}
+ />
+
{{#each this.todos key="id" as |todo|}}
<Label
text={{todo.label}}
/>
{{/each}}
</StackLayout>
The TextField
will allow us to input a label for the todo. We read its text value from the newTodoLabel
property, then we call a handleChangeText
action when it’s changed, to store its value.
The Button
will indicate that we’re ready to add the todo, and it calls an addTodo
action.
Now, let’s wire up these properties and actions in TodoGlimmerNative/component.ts
:
import Component from '@glimmer/component';
+import { tracked } from '@glimmer/tracking';
export default class TodoGlimmerNative extends Component {
title = "Welcome to Glimmer Native"
+ newTodoId = 4
+
+ @tracked
+ newTodoLabel = ''
+
+ @tracked
todos = [
{ id: 1, label: 'Buy bread' },
{ id: 2, label: 'Buy milk' },
{ id: 3, label: 'Buy eggs' },
]
+ handleChangeText(event) {
+ this.newTodoLabel = event.value
+ }
+ addTodo() {
+ const newTodo = { id: this.newTodoId, label: this.newTodoLabel }
+ this.todos = [...this.todos, newTodo]
+ this.newTodoId++
+ this.newTodoLabel = ''
+ }
}
Here’s what’s going on here:
- The
tracked
function we import is a decorator that tells Glimmer to rerender when the decorated property changes. newTodoId
allows us to keep track of the ID to assign to the next todo we create. We start with three todo items so our first new ID is 4.newTodoLabel
will store the value of the todo label the user inputs. It starts out as an empty string. When we manually change it in our actions, we want the component to rerender, so we mark it as@tracked
.- We mark the
todos
array as@tracked
as well, because when we add a new todo, we want it to be shown in the component. handleChangeText
takes the text the user has entered and stores it on thenewTodoLabel
property so it’s available for us to use inaddTodo
addTodo
creates a new todo in four steps:- It creates a
newTodo
object with the next ID and the entered label. - The
todos
property is overwritten with a new array, containing all the existing todos, plus the new one added onto the end. By overwriting the array, Glimmer is able to track that it changed. Changes inside of tracked arrays and objects aren’t detected, only reassignments of the tracked property. - We increment
newTodoId
so the next todo we add will have a unique ID. - We clear out the
newTodoLabel
so the user can enter an additional todo.
- It creates a
After your app reloads, tap on the text field, enter a todo name, and tap “Add Todo”. Note that the todo is added to the list. In particular, note that when we changed the values of newTodoLabel
and todos
, the component rerendered to display them on the screen.
Next, let’s add the ability to complete a todo. For the purposes of this app, we’ll just remove the todo when it’s completed.
Edit TodoGlimmerNative/template.hbs
:
{{#each this.todos key="id" as |todo|}}
- <Label
- text={{todo.label}}
- />
+ <FlexboxLayout
+ flexDirection="row"
+ justifyContent="space-between"
+ alignItems="center"
+ >
+ <Label
+ text={{todo.label}}
+ />
+ <Button
+ text="Complete"
+ {{on "tap" (action completeTodo todo)}}
+ />
+ </FlexboxLayout>
{{/each}}
Instead of each todo being a single label, we now need to display a label on the left and a button on the right. To accomplish this, we use a FlexboxLayout
. It’s based on the CSS flexbox layout algorithm. In particular, here, we set the flexDirection
to row
so the items are side-by-side, and we set justifyContent
to space-between
so that the two items will be on the left and right, respectively. alignItems
set to center
means that they’ll be centered top-to-bottom with regard to one another.
The second item we add is a Button
hooked up to the completeTodo
action. We pass the todo
for the iteration of the loop into it.
The only change we need to make to TodoGlimmerNative/component.ts
is to add this completeTodo action
:
this.newTodoId++
this.newTodoLabel = ''
}
+
+ completeTodo(todo) {
+ this.todos = this.todos.filter(testTodo => testTodo.id !== todo.id)
+ }
}
We just filter out of todos
the todo matching the ID that’s passed in. This will remove the todo from the list.
When the app reloads, try adding and completing todos.
Splitting Out Components
Our app is functional, but as our app grows, we definitely won’t want everything in one big component. Instead, let’s split our app into a few child components and see how we can pass data and actions around.
First, let’s make a NewTodoForm
component for the text field and Add button. Create a components/NewTodoForm/
folder.
Add NewTodoForm/template.hbs
and copy over the relevant components:
Add NewTodoForm/component.ts
with the related property and actions:
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
export default class NewTodoForm extends Component {
@tracked
newTodoLabel = ''
handleChangeText(event) {
this.newTodoLabel = event.value
}
addTodo() {
const { onAdd } = this.args
onAdd(this.newTodoLabel)
this.newTodoLabel = ''
}
}
The addTodo
is a bit different than before, though. This component doesn’t have access to the todos
list or the newTodoId
, so we can’t create the todo itself here. Instead, we take a passed-in action argument and pass the label to it instead. In Glimmer component arguments are accessible on this.args
.
Let’s update TodoGlimmerNative/template.hbs
to use NewTodoForm
, passing the action to it:
<StackLayout>
+ <NewTodoForm @onAdd={{action addTodo}} />
- <TextField
- hint="New Todo"
- text={{this.newTodoLabel}}
- {{on "textChange" (action handleChangeText)}}
- />
- <Button
- text="Add Todo"
- {{on "tap" (action addTodo)}}
- />
{{#each this.todos key="id" as |todo|}}
Now update TodoGlimmerNative/component.ts
to remove the property and action that were moved to NewTodoForm
, and update the addTodo
action:
export default class TodoGlimmerNative extends Component {
title = "Welcome to Glimmer Native"
newTodoId = 4
-
- @tracked
- newTodoLabel = ''
@tracked
todos = [
{ id: 1, label: 'Buy bread' },
{ id: 2, label: 'Buy milk' },
{ id: 3, label: 'Buy eggs' },
]
-
- handleChangeText(event) {
- this.newTodoLabel = event.value
- }
- addTodo() {
- const newTodo = { id: this.newTodoId, label: this.newTodoLabel }
+ addTodo(label) {
+ const newTodo = { id: this.newTodoId, label }
this.todos = [...this.todos, newTodo]
this.newTodoId++
- this.newTodoLabel = ''
}
completeTodo(todo) {
this.todos = this.todos.filter(testTodo => testTodo.id !== todo.id)
}
}
Now addTodo
receives the label
as a parameter. We still construct the new todo object and add it to the array. The newTodoLabel
property is no longer on the TodoGlimmerNative
component so it doesn’t need to be cleared out here anymore. Now we have a nice separation between the temporary state of the form and the core application state of the list of todos.
After the app reloads, confirm you can still add todos.
Next, let’s extract the TodoList
to a component as well. Create a components/TodoList/
folder.
Create a TodoList/template.hbs
file and copy over the loop:
Note that instead of iterating over this.todos
we iterate over @todos
–this is the Glimmer template syntax for accessing an argument passed to the component. Everything else is the same.
Next, create the corresponding TodoList/component.ts
file and add the following:
import Component from '@glimmer/component';
export default class TodoList extends Component {
completeTodo(todo) {
const { onComplete } = this.args
onComplete(todo)
}
}
When the completeTodo
action is called, we just call the passed-in onComplete
action argument.
Finally, update TodoGlimmerNative/template.hbs
to call TodoList
and pass that action:
<StackLayout>
<NewTodoForm @onAdd={{action addTodo}} />
+ <TodoList @todos={{this.todos}} @onComplete={{action completeTodo}} />
-
- {{#each this.todos key="id" as |todo|}}
- <FlexboxLayout
- flexDirection="row"
- justifyContent="space-between"
- alignItems="center"
- >
- <Label
- text={{todo.label}}
- />
- <Button
- text="Complete"
- {{on "tap" (action completeTodo todo)}}
- />
- </FlexboxLayout>
- {{/each}}
</StackLayout>
The completeTodo
action in TodoGlimmerNative
doesn’t need to be changed; it continues to work as-is.
When the app reloads, confirm you can still complete todos.
Styling
Now that our app is functioning, let’s apply a little bit of styling. We add classes to the components we want to style.
In NewTodoForm/template.hbs
:
<TextField
hint="New Todo"
+ class="new-todo-text"
text={{this.newTodoLabel}}
{{on "textChange" (action handleChangeText)}}
/>
<Button
text="Add Todo"
+ class="add-todo-button"
{{on "tap" (action addTodo)}}
/>
Then, in TodoList/template.hbs
:
<FlexboxLayout
flexDirection="row"
justifyContent="space-between"
alignItems="center"
+ class="todo-row"
>
<Label
text={{todo.label}}
+ class="todo-label"
/>
<Button
text="Complete"
+ class="todo-button"
{{on "tap" (action completeTodo todo)}}
/>
</FlexboxLayout>
Now, in app/app.css
, we add these styles:
.new-todo-text {
font-size: 16pt;
margin: 5pt;
}
.add-todo-button {
height: 44pt;
}
.todo-row {
border-color: gray;
border-bottom-width: 1pt;
height: 44pt;
}
.todo-label {
margin-left: 10pt;
font-size: 16pt;
}
.todo-button {
margin-right: 10pt;
}
Note that these rules are all normal CSS rules from the web. There are some slight differences, though: for example, the border-bottom
shorthand doesn’t work; you have to separately declare border-color
and border-bottom-width
. Also note that we use points instead of pixels as the unit. Unlike web browsers, NativeScript maps px
to device pixels, so if we want to work with “normal” virtual pixels we need to use pt
.
When the app reloads, there’s a lot more breathing room and it looks nicer. With that, our app is done!
Conclusion
Notice a few things about Glimmer-Native:
- We got a declarative UI: our UI was automatically updated each time we changed our data.
- We didn’t have to follow a convention like calling a
useState
hook or setting up adata
property; we just accessed properties on normal JavaScript objects. All we needed was a decorator to indicate which properties to track. - We didn’t need to explicitly import components; they were all automatically available from within templates.
- Portions of templates could be extracted directly into child components; we didn’t need to wrap them with a containing element.
These are all really exciting possibilities for building apps productively. Give Glimmer-Native a try and please provide feedback via GitHub Issues!