Browser engines have evolved enormously and i want to share some ideas how to quickly build a JavaScript
Single Page Application,
using cool browser features like
This will be a very basic project, it will take maybe 15 minutes to get running and we wont need to install any node_modules
just write the code and boom it's ready.
You can view the live
result here and check the full code
here
For developing you should use a server
for serving your index.html
, you can use whatever you like, for example with node.js
you can use http-server
Okay so lets start
Basic setup
First lets create some base files, we need index.html
for sure, so let's start with that.
In your project folder create a new file called index.html
And add some content to it
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>My blog</title>
<link rel="stylesheet" href="Application/styles/index.css">
</head>
<body>
<div id="root"></div>
</body>
<script src="https://cdnjs.cloudflare.com/ajax/libs/webcomponentsjs/1.2.0/webcomponents-lite.js"></script>
<script type="module" src="Application/App.js"></script>
</html>
We have some basic stuff in it like, title, stylesheet, root, and our script tag, i have also included webcomponents-lite.js
as a polyfill to increase the browser support for webcomponents
, cause they still lack support on some browsers.
Lets focus on the script
tag, we have type=module
there, we need to use it if we want to activate ES6 modules
support in our JS
file, so dont forget it.
<script type="module" src="Application/App.js"></script>
Next lets create our folder structure.
Lets make it like this
- Application
- components
- modules
- styles
- App.js
Good lets now move on to writing JavaScript
JavaScript files setup
Our Application starts from App.js
so let's create this first.
Copy all this to your App.js
import Router from "https://unpkg.com/navigo@6.0.2/src/index.js";
import { HTML } from "https://unpkg.com/kelbas";
const router = new Router();
router.on("/", () => {
const element = HTML`<h2>Hello</h2>`;
document.querySelector("#root").replaceWith(element);
});
router.on("/post/:id", params => {});
router.resolve();
Currently when we open our Application we should see Hello
I will explain a bit what our imported
libraries do.
For the Router
we will be using navigo, it's a very light weight awesome routing library for the browsers.
Then we have the HTML library and that's created by myself, it extends on the idea of using ES6 template strings to create HTML
elements.
Next in our components
folder lets create 3 files home.js
, post.js
and index.js
So our components folder looks like this
- components
- home.js
- index.js
- post.js
In our index.js
lets just export
the other 2 files, so we could easily import all our components later.
index.js
export { default as home } from "./home.js"
export { default as post } from "./post.js"
Before we can start building our pages we will add one more thing.
In our modules folder, lets create a file called Element.js
- Application
- modules
- Element.js
- modules
Our Element.js
will be a wrapper for native HTMLElement
, i would recommend using a simple wrapper so we supercharge our development with Custom elements
In the newly created Element.js
add
Element.js
export default class Element extends HTMLElement {
//Element is connected
async connectedCallback() {
this.install.apply(this)
this.beforeRender()
const element = await this.render()
this.appendChild(element)
this.installed()
}
//Element is removed
disconnectedCallback() {
this.uninstall()
}
//Update element
async update() {
const element = await this.render()
this.beforeUpdate()
this.lastChild.replaceWith(element)
this.afterUpdate()
}
//You can use these function to run your functions between component state changes, like with react.js
install() { }
installed() { }
uninstall() { }
beforeUpdate() { }
afterUpdate() { }
beforeRender() { }
}
Creating Web-component pages
Now we have our project fully setup and we can start implementing pages, as this is meant as a short tutorial i will be using JSON-Placeholder for my data
.
We can pull posts
in by just calling
fetch('https://jsonplaceholder.typicode.com/posts/')
.then(response => response.json())
Lets add some JavaScript
to our /components/home.js
home.js
import { HTML } from "https://unpkg.com/kelbas"
import Element from "../modules/Element.js"
export default class Home extends Element {
async getPosts() {
return await fetch("https://jsonplaceholder.typicode.com/posts").then(res => res.json())
}
open(id) {
this.router.navigate(`/post/${id}`)
}
async render() {
const posts = await this.getPosts()
return HTML`<section id="posts-container">
${posts.map((post) => {
return HTML`<div class="post" onclick=${this.open.bind(this, post.id)}>
<h3 class="post-title">${post.title}</h3>
<p class="post-body">${post.body}</p>
</div>`
})}
</section>`
}
}
First we add our required libraries, we import
the Element.js
that we created before and also the HTML parser that i talked about earlier.
If you have developed with React.js
then this could seem quite familiar, which is awesome in my opinion, cause we dont need to bundle JSX
to render this and it basically has the same API
.
We have an async function to pull the data
,
then in the render
function first we wait for the data and then show the data
retrieved, very simple.
We could easily add a Loading...
indicator here but i pass it this time for keeping things simple.
There is also the function open
, just add it currently, we will implement this.router
later in the App.js
Next lets add some JavaScript
to our /components/post.js
post.js
import { HTML } from "https://unpkg.com/kelbas"
import Element from "../modules/Element.js"
export default class Post extends Element {
async getPost() {
return await fetch(`https://jsonplaceholder.typicode.com/posts/${this.post}`).then(res => res.json())
}
async render() {
const {title, body} = await this.getPost()
return HTML`<section id="post-container">
<h2>${title}</h2>
<p>${body}</p>
</section>`
}
}
This is basically the same as home.js
but it will load a single post data, notice the this.post
value in getPost
function, we will add this as an id
in the router
at App.js
Now lets go back to our App.js
, we will implement our newly created components and update the routes
Copy all of this into your App.js
App.js
import Router from "https://unpkg.com/navigo@6.0.2/src/index.js";
import { HTML } from "https://unpkg.com/kelbas";
const router = new Router();
import * as component from "./components/index.js";
for (let key in component) {
component[key].prototype.router = router;
customElements.define(`${key}-element`, component[key]);
}
router.on("/", () => {
const element = HTML`<home-element id="root"></home-element>`;
document.querySelector("#root").replaceWith(element);
});
router.on("/post/:id", params => {
const element = HTML`<post-element id="root"></post-element>`;
element.post = params.id
document.querySelector("#root").replaceWith(element);
});
router.resolve();
First we import
all our components
from /Application/components/index.js
Then we attach our router
to all our components
so we could easily change routes from inside the component
and we also define the customElements as file-name + element
so our home.js
becomes home-element
in custom-elemets
registry.
After defining the customElements
you can attach <home-element></home-element>
to your index.html
and it will work, how cool is that.
We also updated our routes, whats happening there is that when route
is fired, we will create our HTML
customElement
that we imported
and then defined
before.
Then we will use replaceWith on our <div id="root"></div>
element in index.html
.
Also make sure to give your elements something to be identified
with like id
or key
, so we can find the element that we want to replace each time route changes, thats why home-element
and post-element
have id=root
also.
And now the final part add, lets add our designs.
Add styles
In your styles
folder at
- Application
- styles
Lets add index.css
and fill it with some simple css
index.css
@import "https://unpkg.com/mustard-ui@latest/dist/css/mustard-ui.min.css";
@import "https://cdnjs.cloudflare.com/ajax/libs/normalize/8.0.0/normalize.min.css";
body {
width: 60%;
margin: 0 auto;
background: #e67e22;
}
#posts-container {
display: grid;
grid-template-columns: 1fr;
grid-row-gap: 20px;
margin: 50px 0;
}
#posts-container .post {
background: #ecf0f1;
padding: 10px 15px;
cursor: pointer;
}
#post-container {
display: grid;
grid-template-columns: 1fr;
grid-row-gap: 20px;
margin-top:50px;
}
#post-container h2 {
color: #ecf0f1;
}
#post-container p {
color: #ecf0f1;
background: #d35400;
padding: 20px;
}
If you got in trouble you can check the full-code here
I hope my tutorial was helpful and give your thoughts about it in the comments.
Top comments (4)
How about SEO?
There are some articles that google can handle SPA pages fine,
for example check this here
This would be a great task to see how google indexes such a blog, i will try it out.
And to make all routes work when i open a
post/1
for example, i would just forward all routes toindex.html
With
nginx
Cool, very interesting
Thanks for writing this blog -- I was lost trying to connect some web components to a router. You've made it very clear.
Great post. It helped quite a bit. And love your "kelbas" JSX utility!