How ? |
Basic, add your Html |
Implement RxJS from Scratch |
Fetch html files, Generate components |
Router and Pages |
Exemple |
Repo github |
How ?
So, you have already worked on current frameworks like Angular, Vue or React and you wanna build a single page app.
But this time you wanna challenge yourself and build all from strach, if this is not enougth you also wanna implement your own RxJS.
Here we will see how we can do this, simply and efficiently.
So for this we will just use javascript, Html, Css and nodejs for live-refresh
Before if you want you can take a look to :
Basic, add your Html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, shrink-to-fit=no"
/>
<title>Exemple</title>
</head>
<body>
<my-header></my-header>
<div id="app"></div>
<my-footer></my-footer>
</body>
<script type="module" src="./index.js"></script>
<link rel="stylesheet" href="./styles/index.css" />
</html>
<script type="module" src="./index.js"></script>
Cause we import our script like a module, we import any other js file with "import { } from './any.js'" in our js file.
<my-header></my-header>
<my-footer></my-footer>
<div id="app"></div>
This is the container we will use for our single app view.
These Custom elements are undefined for the moment, later you will be able to replace 'my-' with your own naming convention.
Implement RxJS from Scratch
For this part we can say Thanks to Andrea Bertoli for Implements RxJS from Scratch, we will use a lighter version
So,
If you want to implements our own RxJS we need to begin with Subscriptions class, this is a container for functions, including the old one. When you call the unsubscribe
method, each of there functions will be called.
// a container for functions
class Subscription {
constructor() {
this.teardowns = [];
}
add(teardown) {
this.teardowns.push(teardown);
}
unsubscribe() {
this.teardowns.forEach((teardown) => teardown());
this.teardowns = [];
}
}
Operator composition with pipe
The concept of pipe
if simple, it combiens n
functions, calling each with the output of the last one.
This is the logic we need to chain Observables, the pipe
method will return another Observable.
const pipe = (...fns) => (val) => fns.reduce((acc, f) => f(acc), val);
With pipe
, we need to implement the Observable abstraction with a class too.
When we init it, this class requires a function as argument initFunc
, so he use the subscribe function, the initFunc
will be called with the observer as argument.
const pipe = (...fns) => (val) => fns.reduce((acc, f) => f(acc), val);
class Observable {
constructor(initFunc) {
this.initFunc = initFunc;
}
subscribe(observer) {
const subscription = new Subscription();
const subscriber = new Subscriber(observer, subscription);
const teardown = this.initFunc(subscriber);
subscription.add(teardown);
return subscription;
}
pipe(...fns) {
// provide source Obx to each function returned from pipeable operators,
// to start the chaining operation provide the current source Obx (this)
return pipe(...fns)(this);
}
}
At last we need to implement an proxy-like object, the role of Subscriber
class is to stops the event propagation into the wrapped Observer if complete
is called.
// a safe wrapper around observers
export default class Subscriber {
constructor(_observer, _subscription) {
this.observer = _observer;
this.closed = false;
this.subscription = _subscription;
// 1. add an Observer completion logic to the Subscription container
this.subscription.add(() => (this.closed = true)); // <- first function inside the subscription
}
next(value) {
if (!this.closed) {
this.observer.next(value);
}
}
error(err) {
if (!this.closed) {
this.closed = true;
this.observer.error(err);
// 2. enable the Subscriber to call `unsubscribe` on completion
this.subscription.unsubscribe(); // <- unsubscribe on error
}
}
complete() {
if (!this.closed) {
this.closed = true;
this.observer.complete();
this.subscription.unsubscribe(); // <- unsubscribe on completion
}
}
}
Fetch html files | Generate components
Welcome to the fetch part ! Cause we are in local use and without any dependencies, we need to fetch ourself our html files. No, you can't import them like import * as htmlTemplate from './template.html
.
But you know what? Now we had implemented our own RxJS, we can use an Observable instead of a Promise.
We can create a method called _fetchLocal
in a api.js
file, return a new Observable
with the request value.
_fetchLocal
will take the path and the name of the html file you wanna import. (So we can import page and components with the same function).
import Observable from "./../reactive/Observable.js";
export const _fetchLocal = (path, file) => {
return new Observable((observer) => {
const req = new XMLHttpRequest();
const url = `${path}/${file}`;
req.responseType = "text/html";
req.open("GET", url);
req.send();
req.onloadend = (e) => {
observer.next(req);
};
});
};
Now you can create a dom.js
file and put in it the registerComponent
function, this function will take the component Class as argument.
In this function you can customise your own naming convention for component or page (feel free to add your logic to this function)
export const registerComponent = (componentClass) => {
const componentName = `my-${componentClass.name.toLowerCase()}`;
customElements.define(componentName, componentClass);
};
Now we had your base and we can begin to work easier, let's create a compoments folder and create our first compomenents.
In it create files for footer and header (Footer.html
,Footer.js
,Header.html
,Header.js
), don't forget, we use Custom Elements, so in this case we extends our class to HTMLElement
.
In the constructor we fetch the associated template with our _fetchLocal
method.
<!-- footer -->
<div class="footer">
Hey footer
</div>
import { _fetchLocal } from "./../scripts/api/index.js";
// Footer
export default class Footer extends HTMLElement {
constructor() {
super();
_fetchLocal("/components", "Footer.html").subscribe({
next: (data) => {
this.innerHTML = data.response;
},
});
}
}
<!-- header -->
<div class="flex-row">
<nav class="nav">
<a href="/" class="nav__link" data-link>Home</a>
<a href="/dashboard" class="nav__link" data-link>Dashboard</a>
<a href="/thispagedontexist" class="nav__link" data-link
>This page don't exist</a
>
</nav>
</div>
// Header
export default class Header extends HTMLElement {
constructor() {
super();
}
connectedCallback() {
_fetchLocal("/components", "Header.html").subscribe({
next: (data) => {
this.innerHTML = data.response;
},
});
}
}
So, now we need to register our component.
In our main.js create a define
method, we register all of our component in it, for that we use our registerComponent
.
Put the define
method in your init
function.
import Header from "./../components/Header.js";
import Footer from "./../components/Footer.js";
import { _fetchLocal } from "./api/index.js";
import { registerComponent } from "./dom/index.js";
export function init() {
define();
}
function define() {
registerComponent(Header);
registerComponent(Footer);
}
Router and Pages
We had our reactive logic, our components are reconized by the app, now we can attack the Router
and Pages
.
We begin with the easlier, like the components we can create the files for our pages, we also respect the component logic.
So we can create files for 404, Dashboard and Home into a pages
folder. (404.html
,404.js
,Dashboard.html
,Dashboard.js
,Home.html
,Home.js
)
<!-- 404 -->
<div class="404">
<div>Are you lost ?</div>
</div>
import { _fetchLocal } from "./../scripts/api/index.js";
export default class NotFound extends HTMLElement {
constructor() {
super();
_fetchLocal("/components", "404.html").subscribe({
next: (data) => {
this.innerHTML = data.response;
},
});
}
}
<!-- Dashboard -->
<div class="dashboard">
<div>this is dashboard</div>
</div>
import { _fetchLocal } from "./../scripts/api/index.js";
export default class Dashboard extends HTMLElement {
constructor() {
super();
_fetchLocal("/components", "Dashboard.html").subscribe({
next: (data) => {
this.innerHTML = data.response;
},
});
}
}
<!-- Home -->
<div class="home">
<div>this is home</div>
</div>
import { _fetchLocal } from "./../scripts/api/index.js";
export default class Home extends HTMLElement {
constructor() {
super();
_fetchLocal("/components", "Home.html").subscribe({
next: (data) => {
this.innerHTML = data.response;
},
});
}
}
So create a router
folder and index.js
file.
In our index.js
file you can put our routing logic into and routes
Object with path
and component
as key.
Like this :
export const routes = [
{ path: "/", component: "Home" },
{ path: "/dashboard", component: "Dashboard" },
{ path: "/home", component: "Home" },
{ path: "/404", component: "404" },
];
Now we need a Views
class, so we can set the fetched HTML
provided by the view into our #app
container.
In the constructor get the user location pathname, compare it to the path of our routes
, if no match we will show the 404 page.
The getHtml
method return the result of _fetchLocal
and the setView
method put the fetched html into the #app
container.
import { routes } from "./../router/index.js";
import { _fetchLocal } from "./../api/index.js";
export default class Views {
layout;
constructor() {
this.layout = routes.filter((route) => {
return route.path === location.pathname;
})[0] || { component: "404" };
this.getHtml().subscribe({ next: this.setView });
}
getHtml() {
return _fetchLocal("/pages", `${this.layout.component}.html`);
}
setView(data) {
document.querySelector("#app").innerHTML = data.response;
}
}
Comeback to our main.js
file and call an instance of Views
class in the define
function.
import Header from "./../components/Header.js";
import Footer from "./../components/Footer.js";
import { _fetchLocal } from "./api/index.js";
import { registerComponent } from "./dom/index.js";
import Views from "./dom/views.js";
export function init() {
define();
}
function define() {
registerComponent(Header);
registerComponent(Footer);
new Views();
}
For the presentation we can add a little of css, for auto-size and centered layout add this :
html,
body {
height: 100%;
width: auto;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
}
/* reset all */
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
a {
padding: 0.25em;
}
#app {
display: flex;
flex-grow: 1;
align-items: center;
align-self: center;
}
Exemple
Now we can see our result with this online exemple. As you see we had our Header
, our Footer
and the layout of the #app
container use our router logic and show the requested page
. Our single page app is created, is reactive, all it's good !
Thanks for all reader if you had the courage to stand until the end.
Repo github
NOPR9D / html-js-single-app-rx
From scratch, no dependencies. With Router, ...
Single App with RxJS | no dependecies
Use a light reactive starter for your next web project. 🚀
Top comments (1)
ty for the wripteup. iam also in angular/store atm for my latest project.