PlumeJs is a very light weight typescript framework to build spa's. It is build on more accessable web components, typescript and lighterhtml. It comes with features like change detection during async operations, data-sharing via factories and props, dependency injection.
PlumeJs is a conceptual combination of angularjs and react. just like angular one can register services, components, life-cycle hooks, Input 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.
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 and description and will install all your required dependencies.
After all the dependencies were installed, you can run application using command npm start.
Breaking change in 2.2.2 version
There is a breaking change in component declaration. Check below:
// 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';
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 root components will not render the page and throw duplicate root component error.
It is called when there is any change in @Input() property
@Component({
selector: 'person-list'
})
class PersonsList implements IHooks {
@Input()
personsData: IPersonsData = null;
inputChanged(oldValue: IPersonsData, newValue: IPersonsData) {
// do your operation here.
// no need to call `this.update()` here. It may cause undesired results.
// dont have any return value.
}
render(){
...
}
}
Data Sharing
We can even share data between two components as below:
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
Note: The constructor arguments are strictly typed and should not be native types or 'any'. Else they will return undefined.
Routing
PlumeJs uses hash-based Routing. It uses dynamic imports to chunk out route specific logic which reduces main bundle size significantly. Routing can be implemented in 2 simple steps:
Declare routes array as below
import { Router, Route } from 'plumejs';
@Component({
selector: 'app-comp',
root: true
})
class AppComponent {
constructor() {
Router.registerRoutes(this.routes);
}
routes: Array<Route> = [{
path: '',
redirectto: '/home',
},{
path: '/home',
template: '<app-home></app-home>',
templatePath: () => import('<path-to-ts-file-of-home-component>')
},{
path: '/contactus',
template: '<app-contactus></app-contactus>',
templatePath: () => import('<path-to-ts-file-of-contactus-component>')
},{
path: '/details/:id',
template: '<app-details></app-details>',
templatePath: () => import('<path-to-ts-file-of-details-component>'),
// canActivate route gaurd helps to check wheter the route is accesseble or not.
// canActivate function should return Promise<boolean> or Observable<boolean> or boolean.
canActivate: () => {
let key = localStorage.getItem('key');
if(!key) {
this.router.navigateTo('/home');
return false;
}
return true;
}
}]
}
Router.registerRoutes(routes); => Routes must be registered with Router service. In previous version(< 2.0.8), routes are passed as input to router-outlet.
...
add <router-outlet></router-outlet> in your component
That's it. Now we have the routing in our application.
To navigate from one route to other from a component:
import {Router} from './plumejs'
@Component({
selector: '<your-selector></your-selector>'
})
class YourClass {
constructor(private router: Router){}
onclick() {
this.router.navigateTo('/your-route');
}
}
To Access current route parameters
route = [{
path: '/details/:id'
....
}]
...
if window.url is /details/123
const currentRoute = this.router.getCurrentRoute();
const id = currentRoute.params.id; /// returns 123
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 {..., TranslationService} from 'plumejs';
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.
The above object inside template literal contains 'html' key which properly allow lighterhtml to render html properly. This is to address a defect where <div innerHTML=${ 'html-translation'.translate() }></div> won't work properly.
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 here.
CSS Tips
One problem with webcomponents is the css selectors can't penetrate through shadow dom. There will be cases where a particular webcomponent should display in a particular way. In order to do that use:
The main problem with webcomponents when implementing @media css arises if there is no <meta name="viewport" content="width=device-width,initial-scale=1"> meta tag in html page. so they always target viewport dimensions instead of element dimensions. As per observation, with respect to webcomponents, if there is no above mentioned meta tag then there are only 2 break points to implement @media css. They are:
// For tablets and other small screens
@media screen and (max-width: 980px) {
:host(<your-selector>) /deep/ .yourclass | #your-id {
// your styles
}
}
// For desktop and above
@media screen and (min-width: 981px) {
:host(<your-selector>) /deep/ .yourclass | #your-id {
// your styles
}
}
All the media breakpoints will work if the above meta tag is there in html page.
/deep/ is very helpful to penetrate through shadowDom and style the target.
By default all plumejs components are render as block elements. They internally have :host { display: block; } property.