tutorial // Oct 29, 2021
Building and Rendering Your First Joystick Component
How to build a simple app and write a component using CheatCode's `@joystick.js/ui` framework and render it to the browser using `@joystick.js/node`.
Getting Started
For this tutorial, we're going to use CheatCode's full-stack JavaScript framework, Joystick. Joystick brings together a front-end UI framework with a Node.js back-end for building apps.
To begin, we'll want to install Joystick via NPM. Make sure you're using Node.js 16+ before installing to ensure compatibility (give this tutorial a read first if you need to learn how to install Node.js or run multiple versions on your computer):
Terminal
npm i -g @joystick.js/cli
This will install Joystick globally on your computer. Once installed, next, let's create a fresh project:
Terminal
joystick create app
After a few seconds, you will see a message logged out to cd
into your new project and run joystick start
:
Terminal
cd app && joystick start
After this, your app should be running and we're ready to get started.
Creating the component
When you created your app, if you open up the package.json
file at the root of the project, you will see two dependencies listed: @joystick.js/ui
and @joystick.js/node
. Even though these are separate pacakges, they're designed to work together. To make that happen, we use the @joystick.js/cli
package installed above. When we ran joystick start
above, that connection was established.
In the project that we created, you will see a folder /ui
at the root of the project with three folders inside of it: /ui/components
, /ui/layouts
, and /ui/pages
. When creating components in Joystick using the @joystick.js/ui
package, we use these three types to stay organized:
/ui/components
contains miscellaneous Joystick components that are intended to be rendered alongside other components or composed together in pages./ui/layouts
contains Joystick components that are meant to be wrappers that render static content (e.g., navigation elements or a footer) along with a dynamic page./ui/pages
contains Joystick components that represent pages or URLs in our application that are intended to be compositions of HTML and other components mapped to a route.
For this tutorial, we're going to focus on the last type, pages. The page we're going to create will render some dummy elements for us to demonstrate all of the features of a Joystick component.
First, let's create the folder and file for the component. We'll call it dashboard and store it in /ui/pages/dashboard/index.js
:
/ui/pages/dashboard/index.js
import ui from '@joystick.js/ui';
const Dashboard = ui.component({
render: () => {
return `
<div class="dashboard">
<h4>Dashboard</h4>
</div>
`;
},
});
export default Dashboard;
To kick things off, we want to set up a skeleton for our component. Above, we're importing the ui
object exported from the @joystick.js/ui
package we hinted at earlier. To set up our component, we create a new variable Dashboard
and assign it to a call to ui.component()
, passing an object containing the definition for our component. At the bottom of our file, we make sure to export the Dashboard
variable as the default as Joystick requires us to do this (we'll see why in a bit).
Focusing on the render
property we've set on the object passed to ui.component()
, this is assigned to a function which is responsible for rendering the HTML markup for our component. In Joystick, components are built with pure HTML. Any HTML that you'd write in a plain .html
file will work in a Joystick component.
In our render()
function, we return a string—written using backticks ``
so that we can take advantage of JavaScript string interpolation (allowing us to embed dynamic values like variables or the result of calling a function inside of our HTML).
Inside of that string, we write the HTML for our component—here, just a <div></div>
tag with a class and an <h4></h4>
tag inside of that to get us started. Though it may not look like much, if we were to render this now, we'd see our <h4></h4>
rendered on screen.
Before we do that, let's flesh out our HTML a bit more and add in some CSS:
/ui/pages/dashboard/index.js
import ui from '@joystick.js/ui';
const Dashboard = ui.component({
css: `
.dashboard {
width: 100%;
max-width: 1000px;
margin: 0 auto;
}
.dashboard h4 {
margin-bottom: 20px;
}
.dashboard input {
display: block;
padding: 20px;
font-size: 16px;
border: 1px solid #ddd;
margin-bottom: 20px;
}
.dashboard button {
border: none;
background: #000;
color: #fff;
font-size: 16px;
padding: 20px;
border-radius: 3px;
}
`,
render: () => {
return `
<div class="dashboard">
<h4>Dashboard</h4>
<input type="text" />
<button class="say-hello">Say Hello</button>
</div>
`;
},
});
export default Dashboard;
Same component, just adding a few things. Down in the render()
, we've added an <input />
and a <button></button>
(we'll put these to use in a bit). The important part here is the new css
property.
Again, using ``
backticks (in addition to interpolation, this allows us to do a multi-line string in JavaScript), we've written some CSS for the markup down in our render()
function.
The idea here is that we want to isolate CSS on a per-component basis. This keeps us organized, but also avoids style collisions when using a single CSS file (or multiple CSS files imported into a single file).
Behind the scenes, when our component is rendered, Joystick will take this CSS and automatically scope it to our component. This is how we avoid issues with the cascade in CSS creating overlapping or breaking styles. Styles are directly mapped to your component.
In addition to dynamic scoping, Joystick will also automatically inject this CSS into the <head></head>
of the HTML we render in the browser, meaning styles are automatically rendered alongside your component's HTML. Focusing on the CSS itself, notice that we're referencing elements and class names inside of our component's HTML—no need for anything special; Joystick will handle the tricky stuff for us.
/ui/pages/dashboard/index.js
import ui from '@joystick.js/ui';
const Dashboard = ui.component({
state: {
name: 'Friend',
},
methods: {
sayHello: (component) => {
window.alert(`Hello, ${component.state.name}!`);
},
},
css: `
...
`,
render: ({ state }) => {
return `
<div class="dashboard">
<h4>Dashboard</h4>
<p>I'm going to say "Hello, ${state.name}!"</p>
<input type="text" />
<button class="say-hello">Say Hello</button>
</div>
`;
},
});
export default Dashboard;
Moving forward, next, to make our component interactive we're going to add a generic function to our component known as a method. The methods
property here is assigned an object with custom-named functions that can be called from elsewhere in the component. Each method that we define is passed the entire component
instance as the last available argument (e.g., if we called a method and passed it a value, that value would become the first argument and component
would become the second).
Here, we're defining a method sayHello
that we want to display an alert dialog when called. Inside, we want it to display a message that says "Hello, <name>
is the current value of the name
property on the component's state
object.
Inside of a Joystick component, state
represents the current visual state of the component (think "visual state of affairs"). That state
can be data, settings for part of our UI—anything you'd like. To initialize our state
value (also known as setting our "default" state), we add a state
option to our component, also passed an object, with the names of the values we want to set on state
when the component loads up.
For our component, we want to set name
on state
. Here, we set the default value to 'Friend'
. So it's clear, if we were to call the sayHello
function as-is, we'd see an alert box pop up that said "Hello, Friend!" Let's wire that up now using our component's lifecycle
methods.
/ui/pages/dashboard/index.js
import ui from '@joystick.js/ui';
const Dashboard = ui.component({
state: {
name: 'Friend',
},
lifecycle: {
onMount: (component) => {
component.methods.sayHello();
},
},
methods: {
sayHello: (component) => {
window.alert(`Hello, ${component.state.name}!`);
},
},
css: `
...
`,
render: ({ state }) => {
return `
<div class="dashboard">
<h4>Dashboard</h4>
<p>I'm going to say "Hello, ${state.name}!"</p>
<input type="text" />
<button class="say-hello">Say Hello</button>
</div>
`;
},
});
export default Dashboard;
A Joystick component goes through several "stages of life" when we render it in the browser, what we refer to as its lifecycle. Here, we're adding an object to our component lifecycle
which can be assigned three functions:
onBeforeMount
a function that's called immediately before a Joystick component is rendered in the browser.onMount
a function that's called immediately after a Joystick component is rendered in the browser.onBeforeUnmount
a function that's called immediately before a Joystick component is removed from the browser.
To demonstrate our sayHello
method, we're going to utilize the onMount
lifecycle method/function (the name "method" is the term used to describe a function defined on an object in JavaScript) to call it. All lifecycle
methods are passed the component
instance, which means we can access our methods
via that object. Inside of our onMount
function, we call to component.methods.sayHello()
to say "when this component is rendered on screen, display an alert window and greet the user."
Almost done. To wrap up our component before we move on to routing, the last thing we want to do is wire up some DOM event handlers.
/ui/pages/dashboard/index.js
import ui from '@joystick.js/ui';
const Dashboard = ui.component({
state: { ... },
lifecycle: { .. },
methods: { ... },
css: `
...
`,
events: {
'keyup input': (event, component) => {
component.setState({ name: event.target.value });
},
'click .say-hello': (event, component) => {
component.methods.sayHello();
},
},
render: ({ state }) => {
return `
<div class="dashboard">
<h4>Dashboard</h4>
<p>I'm going to say "Hello, ${state.name}!"</p>
<input type="text" />
<button class="say-hello">Say Hello</button>
</div>
`;
},
});
export default Dashboard;
First, let's focus on the events
property we've added to our component. This is how we define and automatically scope DOM event listeners to our component. Listeners are defined by setting a callback function to a property whose name is a string with some DOM event type, followed by a space, followed by the DOM selector to attach the event to.
Here, we're adding two event listeners: first, a keyup
listener on our <input />
and second a click
listener on our <button></button>
using its class name say-hello
. For our keyup event, we want to dynamically update our state.name
value as we type into the input. To do it, we assign two arguments to our function, event
which represents the keyup event from the DOM and component
(our component instance) as the second.
On the component
instance, a .setState()
method is defined which takes an object containing the properties we want to set (or overwrite) on state. In this case, we want to overwrite name
, setting it to the current value of our input. Here, we use the plain JavaScript event.target.value
property to access that value where event.target
equals the HTML element triggering the event and value
being the current value of that target.
Down in our click
event handler, we use the same argument structure, this time skipping usage of the event
and accessing our sayHello()
method via the component.methods
object on our instance. The idea here being that whenever we click our button, our window.alert()
in sayHello()
will be triggered, displaying the most recent value (assuming we've typed something in our input, we'd expect to see that).
Before we move on, we want to call out a minor change to our render()
function's HTML. Notice that we've added a <p></p>
which embeds the current value of state.name
using a JavaScript interpolation expression ${state.name}
. You'll notice that we've used JavaScript destructuring on the render()
function, "plucking off" the state
value from that object. That object is our component instance. Here, we use destructuring to eliminate the need to type component.state
and instead just pluck off state
directly.
That's it for our component definition. Next, let's jump to the server and wire up a route so we can see it in the browser.
Defining a route and using res.render() to render the component
A route is the technical name for a URL that renders something in our application. To define a route, we need to move to the code that runs on the server-side of our application in the index.server.js
file at the root of our project.
/index.server.js
import node from "@joystick.js/node";
import api from "./api";
node.app({
api,
routes: {
"/dashboard": (req, res) => {
res.render("ui/pages/dashboard/index.js");
},
"/": (req, res) => {
res.render("ui/pages/index/index.js", {
layout: "ui/layouts/app/index.js",
});
},
"*": (req, res) => {
res.render("ui/pages/error/index.js", {
layout: "ui/layouts/app/index.js",
props: {
statusCode: 404,
},
});
},
},
});
In a Joystick app, the server-side counterpart to @joystick.js/ui
is @joystick.js/node
. This package is responsible for setting up our backend, specifically, spinning up an instance of Express.js and running an HTTP server for our app (by default, this is started on port 2600 but can be customized if we want). From that package, an object is exported that we've imported in the code above as node
. On that object, we have a function .app()
which is responsible for setting up our back-end.
When we call it, we pass a few different options to it, the one we care about for this tutorial being routes
which is set to an object of routes we want to define in our app. Above, we have two routes pre-defined (these are automatically included by joystick create
via @joystick.js/cli
): /
and *
, an index route and a catch-all, 404 route *
.
The one we care about here is the /dashboard
route we've added (we've chosen this name as it matches the name of the page we defined but we could call this /pizza
if we wanted).
A route defined on the routes
object is nothing more than an Express.js route (e.g., app.get()
). The difference here is purely syntactic and for organization. We define all of our routes together for clarity and to keep our code consistent. Just like with a normal Express.js route, we have a callback function that's called when our route is visited (known as being a "match" for the URL in the browser).
Inside of our callback here, we call to a special function defined by Joystick on the Express res
ponse object, res.render()
, passing in the path to the page we want to render (Joystick requires that we pass the entire path, including the .js
extension). Behind the scenes, Joystick will do a few things automatically:
- Render our component as HTML (known as SSR or server-side rendering) to send back as the initial response to the browser.
- Find the corresponding JS file that's been compiled (meaning, browser-safe code) by
@joystick.js/cli
and embed in the SSR'd HTML. - In
development
, Joystick also includes some utility functions and the HMR (hot module reload) script for automatically refreshing the browser when we change our code. - Locates all of the CSS in our component tree (we only have a single level to our tree, but if we nested components those would be scanned, too) and embeds it in the
<head></head>
tag of our HTML.
With all of this done, the resulting HTML is returned to the browser and rendered for our user. Inside of the browser-safe JavaScript file for our page component, Joystick automatically includes the script necessary for "mounting" our component in the browser.
This is a process known as hydrating. We initially send some dry, server-side rendered HTML back for the initial request and then load some JavaScript in the browser to hydrate that dry HTML by making it interactive again (i.e., loading the dynamic parts of our JavaScript in the browser).
That's it. If we open up our browser and head to http://localhost:2600/dashboard
, we should see our alert dialog display and after clicking okay, see our component. Try typing your name in the box and clicking the "Say Hello" button to see it in action.
Wrapping up
In this tutorial, we learned how to install the Joystick CLI (@joystick.js/cli
), create a new app, and build a Joystick component using @joystick.js/ui
. We learned about the different features of a component like state, CSS, DOM events, and methods as well as how to define a route and render that component via the res.render()
method on the server.