PlumeJs is a very light weight typescript framework to build spa's. It is build on more accessable web components, typescript. It comes with features like change detection during async operations, data-sharing via services and props, dependency injection.
PlumeJs is a conceptual combination of angularjs and react. Just like angular one can register Services, Components and life-cycle hooks. It has setProps and onbindprops for passing data from one component to another and like react update function to update the view after modal updations and a render function to render the component.
PlumeJs has very few syntaxes enabling faster learning curve.
To start with PlumeJs
Yo Plumejs
Plumejs has yeoman generator which provides the entire scaffolding for your project. To start with:
Require Nodejs version 11.0.0 or above
Run npm install -g yo generator-plumejs
After completing installation run yo plumejs in your destination folder. This will ask you about your project name, description and type of bundler for your project. After that it will install all the required dependencies.
After all the dependencies were installed, you can run application using command npm start.
Starter templates
If you don't want to start with yo plumejs and need to use either with webpack or vite specifically then please check
Plumejs is now moving to scoped packages, deprecating older versions. In the process it shed 50% in size and dependencies compared to its previous versions. The core aka @plumejs/core scope is now only ~50KB and acts more as a library instead of a framework. So the devs can plugin router aka @plumejs/router scope for routing, ui aka @plumejs/ui scope for built in controls and more to come.
The new version adds a new Renderer api which, on dependency injected in component constructor, will provide update function to update a component, emitEvent function to emit an output event to pass data from child to parent and shadowRoot to query dom nodes.
It also adds a new ComponentRef api which takes a component class as generic type and
expose setProps function which is used to set input properties of a child component which are declared as ObservedProperties and
getInstance function to get current instance of child component.
useFormFields now returns an array instead of object. This change makes it in-line with useState. This helps in assigning values directly in array instead of creating new variables and reassigning them. Now useFormFields also provides a resetFormFields function which resets all form values. for example:
Previously PlumeJS rely on reflection for DI. But as javascript itself won't provide reflection metadata at minification phase, Dev has to supply that metadata. Well this is a small inconvinience but this enables devs to use their preferred bundlers like rollup/esbuild/vite/swc which won't rely on reflection which inturn reduce the bundle size. PlumeJS will itself move to vite which leads to way smaller builds when compared with webpack. This is still in WIP which needs modifications to plumejs-router and plumejs-ui.
Breaking change in 3.0.0 version
Input decorator is removed in favor of setProps for better type safe of props.
Inorder to update a component previously dev need to declare update property and call it as a function. But now dev needs to inject Renderer and call renderer.update() in the component. This helps linters to not throw error for usage of uninitialized variables and usage of any.
useRef is deprecated. instead use <input ref=${(node) => { this.ref = node; }}/>. this prevents additional chaining like this.ref.current.<do-some-operation> instead user can do this.ref.<do-some-operation> which is more meaningful.
Breaking change from 2.2.2 version
There is a breaking change in component declaration. Check below:
import { Component } from '@plumejs/core';
// import stylesheet in ts file
import componentStyles from './styles.scss';
@Component({
selector: 'my-comp',
styles: componentStyles // older styleUrl is renamed to styles
})
...
The above change enable watch on stylesheets which is not available in older versions.
Documentation will be updated after testing and after release of new version.
If you want to change scss to css/less, check your typings.d.ts file and update module *.scss to *.css/less.
The above implementation will break existing unit tests. To fix them,
import Component, html functions and create component as follows
import { Component, html } from '@plumejs/core';
import testEleStyles from './test-ele.scss';
@Component({
selector: 'test-ele',
styleUrl: testEleStyles,
root: true
})
class TestEle {
test: string;
constructor() {
this.text = 'hello world!';
}
render() {
return html`<div>${this.text}</div>`;
}
}
Note: Through out the entire application there will be only one root component. Adding more than one root component will not render the page and throw duplicate root component error.
For styling one can use css or scss formats. but scss is the most preferred one. By default all plumejs components are render as block elements. They internally have :host { display: block; } property.
import { Component } from '@plumejs/core';
@Component({
selector: 'sample-comp'
})
class SampleComp {
inputField: Ref<HTMLElement> = useRef(null); // deprecated
inputField: HTMLElement;
getRef() {
console.log(this.inputField);
}
render() {
return html`
<div>
<input type="text" ref=${this.inputField} /> // don't use this way
<input
type="text"
ref=${(node) => {
this.inputField = node;
}}
/>
// use ref like this
<button
onclick=${() => {
this.getRef();
}}
>
click
</button>
</div>
`;
}
}
Partial attributes
Partial attributes implementation like conditional css class modification is a breeze.
Examples:
// THE FOLLOWING IS OK 👍
html`<div class=${`foo ${mayBar ? 'bar' : ''}`}>Foo bar?</div>`;
html`<div class=${'foo' + (mayBar ? ' bar' : '')}>Foo bar?</div>`;
html`<div class=${['foo', mayBar ? 'bar' : ''].join(' ')}>Foo bar?</div>`;
html`<div style=${`top:${top}; left:${left};`}>x</div>`;
// THE FOLLOWING BREAKS ⚠️
html`<div style="top:${top}; left:${left};">x</div>`;
html`<div class="foo ${mayBar ? 'bar' : ''}">x</div>`; // this may work in browser but will fail in unit tests
:warning: Always make sure that the order of deps and constructor arguments is same. The system won't check for types at compile time which will cause defects at runtime.
Setting up Internationalization
Adding translations in PlumeJS is a breeze. Checkout below for implementation:
add i18n folder to your src folder (you can name it as per your standards)
src
|- i18n
add translation files to i18n folder
in i18n/en.ts
const locale_en = {
'user': {
'name': 'My name is {name}'
}
}
export default locale_en;
in i18n/fr.ts
const locale_fr = {
'user': {
'name': 'je m`appelle {name}'
}
}
export default locale_fr;
import translation files in root component and pass them to translation service
import { Component, TranslationService } from '@plumejs/core';
import locale_en from '<folder-i18n>/en';
import locale_fr from '<folder-i18n>/fr';
@Component({
selector: 'app-root'
})
class AppComponent {
constructor(translations: TranslationService) {
translations.setTranslate(locale_en, 'en');
translations.setTranslate(locale_fr, 'fr');
translations.setDefaultLanguage('en');
}
}
now translations are setup for english and french languages.
To pass html from translations, no need to follow special ways:
<div>${{ html: 'html-translation'.translate() }}</div>
// previously
<div>${ 'html-translation'.translate() }</div>
// with new version just like normal translation
The above object inside template literal contains 'html' key which properly allow compiler to render html properly. This is to address a defect where <div innerHTML=${ 'html-translation'.translate() }></div> won't work properly.
For normal text translations:
<div>${ 'text-translation'.translate() }</div>
Unit Tests
sample component unit test:
import { TestBed, Fixture } from '@plumejs/core';
import { AppComponent } from 'src';
describe('Plumejs Component', () => {
let appRoot: Fixture<AppComponent>;
let model: AppComponent;
beforeAll(async () => {
appRoot = await TestBed.MockComponent(AppComponent);
model = appRoot.componentInstance;
});
it('should render h1 element', () => {
const h1: any = appRoot.element.querySelector('h1');
expect(h1.innerHTML).toBe('Hello World');
});
it('should return "hello" on button click', () => {
let span = appRoot.element.querySelector('span');
expect(span.innerHTML).not.toContain('hello');
model.greet();
expect(span.innerHTML).toContain('hello');
});
afterAll(() => {
TestBed.RemoveComponent(appRoot);
});
});
As a scoped package one can install @plumejs/router for routing. For documentation check plumejs router repo
UI Components
As an additional provision, @plumejs/ui npm module exposes a comprehensive set of useful ui components like modal dialog, notifications, multi select dropdown, toggle. You can check the documentaion plumejs ui repo.
If you don't want to use typescript but still want to use plumejs? no problem it got you covered. You can refer plumejs-esnext and use any of below formats from dist folder: