This package has been deprecated

    Author message:

    Package is deprecated. Now plumejs moved to scoped package @plumejs/core. Please visit gthub readme for more info.

    plumejs

    2.2.5 • Public • Published

    GitHub contributors PRs Welcome

    npm npm

    PlumeJs logo

    Demo here. Check console logs for further details.

    Example repo

    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.

    For most asked questions, please check QA.md

    To start with PlumeJs

    Yo Plumejs

    Plumejs has yeoman generator which provides the entire scaffolding for your project. To start with:

    1. Require Nodejs version 11.0.0 or above
    2. Run npm install -g yo generator-plumejs
    3. 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.
    4. 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,

    1. run npm i -D jest-transform-stub
    2. add
    {
      ...
      moduleFileExtensions: ['ts', 'html', 'js', 'json'],
      moduleNameMapper: {
        "^.+.(css|styl|less|sass|scss|png|jpg|ttf|woff|woff2)$": "jest-transform-stub"
      }
    }
    

    to your jest.config.js

    Documentation

    Here is a sneak peak into bultins:

    Creating Components

    Creating component is a non-hectic task.

    1. 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.

    For styling one can use css or scss formats.

    Lifecycle Hooks

    IHooks interface provides mount, unmount, inputChanged lifecycle hooks.

    mount Hook

    It is used to perform model data initialization as follows:

    @Component({
      selector: 'person-list'
    })
    class PersonsList implements IHooks {
      data:Array<string> = [];
      constructor(){}
      mount(){
        fetch('persons-api').then(res => res.json()).then(data => {
          this.data = data;
          this.update(); // triggers change detection and update view
        })
      }
    
      alertName(name:string){
        alert(name);
      }
    
      render(){
        return html(`<div>
          <ul>${
            this.data.map((item:string) => html`<li onclick=${()=>{ this.alertname(item); }}>${item}</li>`)
          }</ul>
        </div>`)
      }
    }
    
    

    umount Hook

    It is used to execute any pendending subscriptions as follows:

    @Component({
      selector: 'person-list'
    })
    class PersonsList implements IHooks {
      data:Array<string> = [];
      constructor(){}
      mySubscription: Observable;
    
      mount(){
        this.mySubscription = from(fetch('persons-api').then(res => res.json()));
    
        this.mySubscription.subscribe(data => {
          this.data = data;
          this.update(); // triggers change detection and update view
        });
      }
    
      unmount() {
        this.mySubscription.unsubscribe();
      }
    
      alertName(name:string){
        alert(name);
      }
    
      render(){
        return html(`<div>
          <ul>${
            this.data.map((item:string) => html`<li onclick=${()=>{ this.alertname(item); }}>${item}</li>`)
          }</ul>
        </div>`)
      }
    }
    
    

    inputChanged Hook

    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:

      import { Component, html, Input } from 'plumejs';
    
      @Component({
        selector: 'person-list'
      })
      class PersonsList {
        data:Array<string> = [];
        persondetails:any = {};
        constructor(){}
        mount(){
          fetch('persons-api').then(res => res.json()).then(data => {
            this.data = data;
            this.update(); // triggers change detection and update view
          })
        }
    
        alertName(name:string){
          this.persondetails.name = name;
          this.update();
        }
    
        render(){
          return html`<div>
            <ul>${
              this.data.map((item:string) => html`<li onclick=${()=>{ this.alertname(item); }}>${item}</li>`)
            }</ul>
            <person-details userdetails=${this.persondetails}></person-details>
          </div>`
        }
      }
    
      @Component({
        selector: 'person-details'
      })
      export class PersonDetails implements IHooks {
    
        @Input()
        userdetails:any = {};
    
        inputChanged(oldValue: any, newValue: any) {
          console.log('oldvalue: ', oldValue);
          console.log('newvalue: ', newValue);
        }
    
        render(){
          return html`${
            <div>${this.userdetails.name}</div>
          }`
        }
      }
    
    

    Just like react, PlumeJs provides useRef as references. example:

    import {Component, Ref, useRef}
    @Component({
      selector: 'sample-comp'
    })
    class SampleComp {
        inputField:Ref<HTMLElement> = useRef(null);
        
        getRef(){
            console.log(this.inputField);
        }
    
      render() {
        return html`
         <div>
                <input type='text' ref=${this.inputField} />
          <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
    

    For more documentation check here

    Hooks

    useFormFields

    useFormFields is very helpful to build forms and retrive form data.

    example:

    import {..., useFormFields} from 'plumejs';
    import {IMultiSelectOptions} from 'plumejs-ui';
    
    @Component(...)
    class SampleForm {
      sampleformFields: any;
        createChangeHandler: any;
        multiSelectChangehandler: any;
      multiSelectOptions: IMultiSelectOptions = {
            data: ['option1', 'option2', 'option3', 'option4'],
            multiple: true,
            onchange: (optionsArr: string[]) => {
                this.multiSelectChangehandler({
                    target: { 
                        value: optionsArr
                    }
                });
            },		
            buttonText: (options:Array<string>) => {
                if (options.length === 0) {
                    return 'None selected';
                }
                else if (options.length > 3) {
                    return options.length + ' selected';
                } else {
                    return options.join(', ');
                }
            },
        }
    
      constructor() {
        const { formFields, createChangeHandler } = useFormFields({
                email: "",
                checkme: false,
                option: '',
                options: [],
                gender: "",
            });
            this.sampleformFields = formFields;
            this.createChangeHandler = createChangeHandler;
            this.multiSelectChangehandler = this.createChangeHandler('options');
            this.submitForm = this.submitForm.bind(this);
      }
    
      submitForm(e: Event) {
            e.preventDefault();
            console.log(this.sampleformFields);
        }
    
      render() {
        return html`
          <form onsubmit=${this.submitForm}>
            <div>
              <label>textbox</label>
              <input onchange=${this.createChangeHandler("email")}/>
            </div>
            <div>
              <b>radio</b>
              <input
                                type="radio"
                                id="gender_male"
                                name="gender"
                                value="male"
                                onchange=${this.createChangeHandler("gender")}
                            />
                            <label for="gender_male">Male</label>
                            <input
                                type="radio"
                                id="gender_female"
                                name="gender"
                                value="female"
                                onchange=${this.createChangeHandler("gender")}
                            />
                            <label for="gender_female">Female</label>
            <div>
            <div>
              <label>checkbox</label>
              <input
                                type="checkbox"
                                name="gender"
                                value="male"
                                onchange=${this.createChangeHandler("checkme")}
                            />
            <div>
            <div>
              <label>single select</label>
              <select value=${this.sampleformFields.option} onchange=${this.createChangeHandler("option")}>
                <option value="1">1</option>
                <option value="2">2</option>
                <option value="3">3</option>
                <option value="4">4</option>
              </select>
            <div>
            <div>
              <label>multi select</label>
              <select value=${this.sampleformFields.option} onchange=${this.createChangeHandler("option")}>
                <option value="1">1</option>
                <option value="2">2</option>
                <option value="3">3</option>
                <option value="4">4</option>
              </select>
            <div>
            <div>
              <label>plumeui component multi select</label>
              <multi-select multiSelectOptions=${ this.multiSelectOptions }></multi-select>
            <div>
            <button type="submit" class="button is-info">Submit</button>
          </form>
        `
      }
    }
    

    Creating Services

    Creating service is as simple as creating a component

      import { Injectable } from './plumejs';
    
      @Injectable()
      export class PersonService {
        getPersons() {
          return fetch('persons-api').then(res => res.json());
        }
      }
    
      // in component
    
      @Component({
        selector: 'test-ele'
      })
      class TestEle {
        test:string;
        data:Array<string> = [];
        constructor(private personSrvc:PersonService){
          this.text = 'hello world!'
        }
    
        mount(){
          this.personSrvc.getPersons().then(data => {
            this.data = data;
            this.update(); // triggers change detection and update view
          })
        }
    
        ...
      }
    

    Services in PlumeJs are singleton

    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:

    1. 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.
    
      ...
    
    1. 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:

    1. add i18n folder to your src folder (you can name it as per your standards)
    src
     |- i18n
     
    
    1. 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;
    
    1. 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');
      }
    }
    
    1. now translations are setup for english and french languages.

    2. To pass html from translations:

    <div>${{html: 'html-translation'.translate() }}</div>
    

    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.

    For normal text translations:

    <div>${ 'text-translation'.translate() }</div>
    

    Unit Tests

    1. sample component unit test:
    import { TestBed } from 'plumejs';
    import { AppComponent } from 'src';
    
    describe("Plumejs Component", () => {
    
      let appRoot:any;
    
        beforeAll(async () => {
        appRoot = await TestBed.MockComponent(AppComponent);
      });
      
      it('should render h1 element', () => {
        const h1:any = appRoot.querySelector('h1');
        expect(h1.innerHTML).toBe("Hello World");
      });
    
      it('should return "hello" on button click', () => { 
        let span = appRoot.querySelector('span');
        const model:AppComponent = appRoot.getModel();
        expect(span.innerHTML).not.toContain('hello');
        model.greet();
        expect(span.innerHTML).toContain('hello');
      });
    
      afterAll(()=>{
        TestBed.RemoveComponent(appRoot);
      });
    });
    
    
    1. sample service unit test:
    class SampleService {
    
        data: any = {};
    
        callApi() {
            return fetch("https://jsonplaceholder.typicode.com/users").then((res:any) => res.json()).then((res: any) => {
                this.data = res;
            });
        }
    }
    
    describe("Plumejs Service", () => {
    
        let servc: SampleService;
        let _fetch: any = fetch;
    
        beforeAll(() => {
            servc = new SampleService();
        });
    
        beforeEach(() => {
            _fetch.resetMocks();
        });
    
        it("should work",async () => {
        _fetch.mockResponseOnce(JSON.stringify({
          animals: ["cow", "goat", "lion"]
        }));
            await servc.callApi();
            expect(servc.data.animals.length).toBe(3);
        });
    });
    
    

    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 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:

    :host-context(your-dom-tag-name | .your-class | #your-id) {
      // your styles
    }
    
    (or)
    
    :host(your-dom-tag-name | .your-class | #your-id) {
      // your styles
    }
    

    Responsive webcomponents

    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.

    An example repo can be found here for reference.

    In the memory of my beloved cousin 💓 💓 💓 Pushpak Ganti

    Install

    npm i plumejs

    DownloadsWeekly Downloads

    2

    Version

    2.2.5

    License

    MIT

    Unpacked Size

    107 kB

    Total Files

    51

    Last publish

    Collaborators

    • kiranmantha