When building StencilJS applications (whether you are using Ionic with it or not) you will be using JSX to create the templates for your components. JSX is a syntax extension to standard ECMAScript (JavaScript) that adds additional XML style syntax. It was created by Facebook and popularised by its usage in the React framework, but JSX can be used outside of React - which is obviously the case for StencilJS.
In over-simplified terms, JSX allows you to mix HTML with JavaScript. If you’ve never used it, it’s definitely one of those kind of things that feels a little weird to get used to at first. The purpose of this article is to introduce you to the basics of JSX and how it would be used in StencilJS application, to help get you over that initial confusion hump.
Since many of the readers of this blog will already be familiar with Angular, I will make some references and comparisons to Angular throughout this article. However, if you are not familiar with Angular you will still be able to read the article (just ignore the references).
If you haven’t already been using something like React to build applications, then chances are you would be used to defining standard-looking HTML files to contain your template e.g:
my-template.html
<div>
<p>Hello</p>
</div>
Or if you were defining templates inside of your JavaScript/TypeScript files then you would typically define a template string like this:
template: `
<div>
<p>Hello</p>
</div>
`;
Either way, we are just using standard syntax in these cases. However, when you start using JSX, you will also be defining your templates inside of your JavaScript/TypeScript files but it will look something like this:
render() {
return <div><p>Hello</p></div>;
}
or this:
render(){
return (
<div>
<p>Hello</p>
</div>
)
}
and maybe you even have some variables defined with HTML syntax attached to them:
const myMessage = <p>Welcome to my app!</p>;
If you’ve never used JSX then the examples above look like they would cause a bunch of syntax errors. We aren’t defining HTML inside of strings, we are just straight-up writing HTML inside of a JavaScript/TypeScript file. But, mixing HTML and JavaScript like this is exactly what JSX allows.
We are going to go through some examples that cover the basics of how to use JSX. Although this is in the context of a StencilJS application, the same concepts apply to JSX as a general concept.
A Basic Template
When building an application with StencilJS, we will have various components that make up our application. These components will each define a render
function that specifies the template.
The render
function simply needs to return the template, which will use JSX, that the component should use. This will look something like this:
import { Component } from '@stencil/core';
@Component({
tag: 'app-profile',
styleUrl: 'app-profile.css',
})
export class AppProfile {
render() {
return (
<div>
<p>Hello</p>
</div>
);
}
}
The render function should return
the JSX template, but it is also possible to add additional code inside of the render
function (which could be utilised inside of the JSX template):
import { Component } from '@stencil/core';
@Component({
tag: 'app-test',
styleUrl: 'app-test.css'
})
export class AppTest {
const myMessage = <p>Welcome to my app!</p>;
render(){
return (
<div>
<p>{myMessage}</p>
</div>
)
}
}
Returning Multiple Root Nodes
The render
function must return a single root node, that means that this is will work:
return <div></div>;
and this will work:
return (
<div>
<p>Hello</p>
<div>
<p>there.</p>
</div>
</div>
);
but this will not:
return (
<div>Root one</div>
<div>Root two</div>
)
To address this scenario you can either wrap the two nodes inside of a single node:
return (
<div>
<div>Root one</div>
<div>Root two</div>
</div>
);
or you can return an array of nodes like this:
return [<div>Root one</div>, <div>Root two</div>];
Notice the use of square brackets to create an array, and that each node is followed by a comma as in an array. Although it may look a little strange to use HTML syntax in an array like this, it is the same idea as doing this:
return ['array element 1', 'array relement 2'];
Expressions
Expressions allow us to do things like execute logic inside of our templates or render out a variable to display on the screen. If you are coming from an Angular background, the idea is basically the same as interpolations with double curly braces, except that we just use a single curly brace like this:
render(){
return (
<div>
<p>{ 1 + 1 }</p>
</div>
)
}
This example would execute the 1 + 1
operation, and display the result inside of the paragraph tag. In this case, it would therefore render the following out to the DOM:
<div>
<p>2</p>
</div>
We could also use expressions to make function calls:
calculateAddition(one, two){
return one + two;
}
render(){
return (
<div>
<p>{this.calculateAddition(1,5)}</p>
</div>
)
}
Render out local or class member variables:
public myNumber: number = 1;
render(){
const message = <p>Hi</p>;
return (
<div>
<p>{message}</p>
<p>{this.myNumber}</p>
</div>
)
}
and much more, some of which we will touch on later. A difference you might notice here if you are coming from an Angular background is that inside of the curly braces we need to reference the this
scope in order to access the member variable, which isn’t the case with Angular templates.
Styles
If you attempt to add a style to an element in JSX like this:
render(){ return (
<div style="background-color: #f6f6f6; padding: 20px;">
<p>Hello</p>
</div>
) }
You will be met with an error that reads something like this:
Type 'string' is not assignable to type '{ [key: string]: string; }'.
But what’s the deal with that? Aren’t we allowed to use HTML syntax? Not quite, there are a few differences. With JSX, inline styles must be supplied as an object, where the properties of that object define the styles you want:
render(){
return (
<div style={{
backgroundColor: `#f6f6f6`,
padding: `20px`
}}>
<p>Hello</p>
</div>
)
}
We are assigning an expression
(which we just discussed) to style
that contains an object representing our style properties. You will notice that we are using camelCase
- so instead of using the usual hyphenated properties like background-color
or list-style-type
we would use backgroundColor
and listStyleType
.
Conditional Templates
There are different ways to achieve conditionally displaying data/elements with JSX, so let’s take a look at a few. We covered before how you could make a function call inside of an expression in your template, and that is one way that you could conditionally display an element:
import { Component } from '@stencil/core';
@Component({
tag: 'app-test',
styleUrl: 'app-test.css'
})
export class AppTest {
private loggedIn: boolean = false;
getWelcomeMessage(){
if(this.loggedIn){
return 'Welcome back!';
} else {
return 'Please log in';
}
}
render(){
return (
<div>
<p>{this.getWelcomeMessage()}</p>
</div>
)
}
}
You could also achieve the same effect by building some logic directly into the expression in the template, rather than having a separate function:
render(){
return (
<div>
<p>{this.loggedIn ? 'Welcome back!' : 'Please log in.'}</p>
</div>
)
}
You could render entirely different chunks of your template using if/else
statements:
import { Component } from '@stencil/core';
@Component({
tag: 'app-test',
styleUrl: 'app-test.css'
})
export class AppTest {
private loggedIn: boolean = false;
render(){
if(this.loggedIn){
return (
<div>
<p>Welcome back!</p>
</div>
)
} else {
return (
<div>
<p>Please log in.</p>
</div>
)
}
}
}
You can completely remove any rendering to the DOM by returning null
instead of an element:
import { Component } from '@stencil/core';
@Component({
tag: 'app-test',
styleUrl: 'app-test.css'
})
export class AppTest {
private loggedIn: boolean = false;
render(){
if(this.loggedIn){
return (
<div>
<p>Welcome back!</p>
</div>
)
} else {
return null
}
}
}
If you don’t want to return entirely different templates depending on some condition (this could get messy in some cases) you could also render entirely different chunks just by using a ternary operator inside of your template like this:
import { Component } from '@stencil/core';
@Component({
tag: 'app-test',
styleUrl: 'app-test.css'
})
export class AppTest {
private loggedIn: boolean = false;
render(){
return (
<div>
{
this.loggedIn
?
<div>
<h2>Hello</h2>
<p>This is a message</p>
</div>
:
<div>
<h2>Hello</h2>
<p>This is a different message</p>
</div>
}
</div>
)
}
}
If you aren’t familiar with the ternary operator, we are basically just looking at this:
this.loggedIn ? 'logged in' : 'not logged in';
which is a simplified version of:
if (this.loggedIn) {
return 'logged in';
} else {
return 'not logged in';
}
We can also simplify the ternary operator more if we don’t care about the else
case. For example, if we only wanted to show a message to a user who was logged in, but we didn’t care about showing anything to a user who isn’t, we could use this syntax:
import { Component } from '@stencil/core';
@Component({
tag: 'app-test',
styleUrl: 'app-test.css'
})
export class AppTest {
private loggedIn: boolean = true;
render(){
return (
<div>
{
this.loggedIn &&
<div>
<h2>Hello</h2>
<p>This is a message</p>
</div>
}
</div>
)
}
}
This will only render out the message if loggedIn
is true
. The methods I have mentioned here should be enough to cover most circumstances, but there are still even more you can use. It’s a good idea to have a bit of a search around and see what kinds of methods suit you best.
Looping over Data
We will often want to create templates dynamically based on some array of data like an array of todos
or posts
and so on. In Angular, this is where you would use an *ngFor
structural directive to loop over that data. However, with StencilJS and JSX we once again just use standard JavaScript syntax/logic embedded directly into the template.
We can use the map
method on an array of data to loop through the data and render out a part of the template for each iteration. Let’s take a look at an example from the documentation:
render() {
return (
<div>
{this.todos.map((todo) =>
<div>
<div>{todo.taskName}</div>
<div>{todo.isCompleted}</div>
</div>
)}
</div>
)
}
In this example, we would have a class member named todos
that we are looping over. For each todo
we will render this out to the DOM:
<div>
<div>{todo.taskName}</div>
<div>{todo.isCompleted}</div>
</div>
The {todo.taskName}
and {todo.isCompleted}
expressions used here will be executed and the values for the paticular todo
in that iteration of the map method will be used.
In order for StencilJS to be able to perform as efficiently as possible, it is important that if you intend to change this data (e.g. you can add/remove todos
) that you give each todo
that is rendered out a unique key
property. You can attach that key
to the root node of whatever you are rendering out for each iteration, e.g:
render() {
return (
<div>
{this.todos.map((todo) =>
<div key={todo.id}>
<div>{todo.taskName}</div>
<div>{todo.isCompleted}</div>
</div>
)}
</div>
)
}
Event Binding
The last thing we are going to cover is event binding, which we will use mostly to handle users clicking on buttons or other elements. We can handle DOM events by binding to properties like onClick
. For example, if we wanted to run a method that we created called handleClick()
when the user clicks a button we might do something like this:
<ion-button onClick={this.handleClick(event)}>Click me</ion-button>
This is fine, and would invoke our handleClick
method, but the downside of this approach is that it won’t maintain the scope of this
. That means that if you were to try to reference a member variable like this.loggedIn
inside of your handleClick
method it wouldn’t work.
You can solve this issue in either of the following ways. You could manually bind
the function to the correct scope like this:
<ion-button onClick={this.handleClick(event).bind(this)}>Click me</ion-button>
or you could use an arrow function like this (which is the more popular approach):
<ion-button onClick={(event) => this.handleClick(event)}>Click me</ion-button>
As well as onClick
you can also bind to other DOM events like onChange
and onSubmit
. If you are using Ionic, then you could also bind to any of the events that the Ionic web components emit.
Summary
As always, there is still much to learn. However, the concepts we have covered in this article should be enough to get you started with JSX and get you used to the initial oddities of using JSX.
For further learning, I would recommend reading the JSX section in the StencilJS documentation as well as the React documentation for JSX. Not all of the React documentation will apply to StencilJS, but it is a good way to gain a broader understanding of JSX in general.