UNPKG

@yauheni-shcharbakou/rxspa

Version:
301 lines (228 loc) 7.49 kB
# 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 [![npm version](https://img.shields.io/npm/v/@yauheni-shcharbakou/rxspa.svg)](https://npmjs.org/package/@yauheni-shcharbakou/rxspa) [![npm license](https://img.shields.io/npm/l/@yauheni-shcharbakou/rxspa.svg)](https://npmjs.org/package/@yauheni-shcharbakou/rxspa) [![npm type definitions](https://img.shields.io/npm/types/@yauheni-shcharbakou/rxspa)](https://npmjs.org/package/@yauheni-shcharbakou/rxspa) ## Installation Install via npm: ```shell npm install @yauheni-shcharbakou/rxspa @yauheni-shcharbakou/rxspa-webpack ``` Install via yarn: ```shell yarn add @yauheni-shcharbakou/rxspa @yauheni-shcharbakou/rxspa-webpack ``` Add `"experimentalDecorators": true` setting to your `tsconfig.json` ## Setup First, declare a type of application context, using Store classes ```typescript import { IStream } from '@yauheni-shcharbakou/rxspa'; export interface IMainState { first: number; second: number; } export interface IMainStore { first: IStream<number>; second: IStream<number>; reset(): void; } ``` ```typescript 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; } } ``` ```typescript import { IMainStore } from './interfaces'; export type AppContext = { main: IMainStore; }; ``` Then declare an app configuration object ```typescript 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 ```typescript 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) ```typescript import { Page } from '@yauheni-shcharbakou/rxspa'; import { AppContext } from '../shared/types'; export default class AppPage extends Page<AppContext> { // Declare additional logic, if needed } ``` ```typescript import { Modal } from '@yauheni-shcharbakou/rxspa'; import { AppContext } from '../shared/types'; export default class AppModal extends Modal<AppContext> { // Declare additional logic, if needed } ``` ```typescript 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 ```typescript import { bootstrap } from '@yauheni-shcharbakou/rxspa'; import { App, appConfig, appContext } from './app'; bootstrap(new App(appConfig, appContext)); ``` ## Usage ##### Page main.page.html: ```handlebars <div> <h2>{{ title }}</h2> <div class="component"></div> </div> ``` main.page.scss: ```css /* any scss-code or css-code */ ``` main.page.ts: ```typescript 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: ```handlebars <p> <span class="first">{{ first }}</span> <br /> <span class="second">{{ second }}</span> <br /> </p> ``` card.component.scss: ```css /* any scss-code or css-code */ ``` card.component.ts: ```typescript 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](https://www.npmjs.com/package/@yauheni-shcharbakou/rxspa-webpack) package ## Other [More examples](https://github.com/yauheni-shcharbakou/npm-packages/blob/main/examples/rxspa)