@yauheni-shcharbakou/rxspa
v1.0.0
Published
Reactive SPA framework
Readme
rxspa
Reactive SPA framework
My first framework, created in November 2021, when I worked on some tasks at Rolling Scopes School. One of the requirements for the tasks in those courses was the prohibition of using frontend frameworks, which led to the appearance of this one in order to circumvent this restriction (nobody forbade using their own frameworks :)) The codebase is preserved in its original form, except edits necessary for publishing and using modern assembly methods and CI
Installation
Install via npm:
npm install @yauheni-shcharbakou/rxspa @yauheni-shcharbakou/rxspa-webpackInstall via yarn:
yarn add @yauheni-shcharbakou/rxspa @yauheni-shcharbakou/rxspa-webpackAdd "experimentalDecorators": true setting to your tsconfig.json
Setup
First, declare a type of application context, using Store classes
import { IStream } from '@yauheni-shcharbakou/rxspa';
export interface IMainState {
first: number;
second: number;
}
export interface IMainStore {
first: IStream<number>;
second: IStream<number>;
reset(): void;
}import { IMainState, IMainStore } from '../shared/interfaces';
import { StoreKey } from '../shared/enums';
import { DEFAULT_MAIN_STATE } from '../shared/defaults';
import { IStream, Store, Stream } from '@yauheni-shcharbakou/rxspa';
export default class MainStore extends Store<IMainState> implements IMainStore {
protected defaultState: IMainState = DEFAULT_MAIN_STATE;
first: IStream<number> = new Stream<number>(DEFAULT_MAIN_STATE.first, StoreKey.MainFirst);
second: IStream<number> = new Stream<number>(DEFAULT_MAIN_STATE.second, StoreKey.MainSecond);
reset(): void {
this.first.value = this.defaultState.first;
this.second.value = this.defaultState.second;
}
}import { IMainStore } from './interfaces';
export type AppContext = {
main: IMainStore;
};Then declare an app configuration object
import { AppConfig } from '@yauheni-shcharbakou/rxspa';
import { AppContext } from '../shared/types';
import { MainPage } from '../pages';
const appConfig: AppConfig<AppContext> = {
entry: MainPage,
modals: {},
pages: {
main: MainPage,
},
root: document.body,
};
export default appConfig;Declare app class
import { Application } from '@yauheni-shcharbakou/rxspa';
import { AppContext } from '../shared/types';
export default class App extends Application<AppContext> {
// Declare additional logic, if needed
}Declare base Page, Modal, Component class (if you need)
import { Page } from '@yauheni-shcharbakou/rxspa';
import { AppContext } from '../shared/types';
export default class AppPage extends Page<AppContext> {
// Declare additional logic, if needed
}import { Modal } from '@yauheni-shcharbakou/rxspa';
import { AppContext } from '../shared/types';
export default class AppModal extends Modal<AppContext> {
// Declare additional logic, if needed
}
import { Component } from '@yauheni-shcharbakou/rxspa';
export default class AppComponent extends Component {
// Declare additional logic, if needed
}Use bootstrap function in your application main file for launch
import { bootstrap } from '@yauheni-shcharbakou/rxspa';
import { App, appConfig, appContext } from './app';
bootstrap(new App(appConfig, appContext));Usage
Page
main.page.html:
<div>
<h2>{{ title }}</h2>
<div class="component"></div>
</div>main.page.scss:
/* any scss-code or css-code */main.page.ts:
import { component, HTMLTemplateVars, useHtml, render } from '@yauheni-shcharbakou/rxspa';
import template from './main.page.html';
import './main.page.scss';
import CardComponent from '../../components/card/card.component';
import { AppPage } from '../../app';
import { HELLO_WORD } from '../../shared/constants';
@component({ template })
export default class MainPage extends AppPage {
private card: CardComponent | null = null;
protected vars(): HTMLTemplateVars {
return { title: HELLO_WORD };
}
protected onInit() {
this.card = new CardComponent(this.router, this.context);
}
protected inject() {
this.node.append(useHtml('<strong>React-style component</strong>'));
if (this.card) {
this.node.querySelector('.component')?.append(render(this.card));
}
}
onDestroy() {
this.card?.onDestroy();
}
}Component
card.component.html:
<p>
<span class="first">{{ first }}</span>
<br />
<span class="second">{{ second }}</span>
<br />
</p>card.component.scss:
/* any scss-code or css-code */card.component.ts:
import { component, render, HTMLTemplateVars } from '@yauheni-shcharbakou/rxspa';
import template from './card.component.html';
import { AppPage } from '../../app';
import './card.component.scss';
import { BtnText } from '../../shared/enums';
import ButtonComponent from '../button/button.component';
@component({ template })
export default class CardComponent extends AppPage {
private firstSpan: HTMLSpanElement = document.createElement('span');
private secondSpan: HTMLSpanElement = document.createElement('span');
protected vars(): HTMLTemplateVars {
return {
first: this.context.main.first.value,
second: this.context.main.second.value,
};
}
private updateFirst(value: number): void {
this.firstSpan.textContent = value.toString();
}
private updateSecond(value: number): void {
this.secondSpan.textContent = value.toString();
}
protected onInit() {
this.context.main.first.subscribe(this.updateFirst.bind(this));
this.context.main.second.subscribe(this.updateSecond.bind(this));
}
protected bindElements() {
this.firstSpan = this.node.querySelector<HTMLSpanElement>('.first') || this.firstSpan;
this.secondSpan = this.node.querySelector<HTMLSpanElement>('.second') || this.secondSpan;
}
protected inject() {
this.node.append(
render(
new ButtonComponent({
title: BtnText.IncFirst,
onClick: () => {
this.context.main.first.value += 1;
},
}),
),
render(
new ButtonComponent({
title: BtnText.IncSecond,
onClick: () => {
this.context.main.second.value += 1;
},
}),
),
render(
new ButtonComponent({
title: BtnText.Reset,
onClick: () => this.context.main.reset(),
}),
),
);
}
onDestroy() {
this.context.main.first.unsubscribe(this.updateFirst.bind(this));
this.context.main.second.unsubscribe(this.updateSecond.bind(this));
}
}Webpack configuration
To bundle it, you can use @yauheni-shcharbakou/rxspa-webpack package
