Skip to content

Custom SPA builded with pure JavaScript using Webcomponents

Notifications You must be signed in to change notification settings

rafaelbatistamarcilio/js-spa

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

38 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Example of a JavaScript SPA (Single Page Aplication)

Created using Angular concepts

ES6 classes

Instalation

mkdir my-project

cd my-project

git pull https://github.com/rafaelbatistamarcilio/js-spa.git .

npm i

npm start

Routing

To add a new route to your project, just add a new entry on the AppRouting class

//app.routing.js

import {MyComponent} from './path/to/my/component'

export class AppRouting {
    static getRoutes() {
        return [
            {
                url: 'my-route',
                component: MyComponent
            }
        ]
    }
}

Components

First create an html file that will be used as the component template

<!-- my-component-template.html -->
<div >
    <button id="my-btn"> Action from component </button>
</div>
<div >
    <p id="my-text"> </p>
</div>

To create a new component you just need to create a ES6 JavaScript class extending BaseComponent

//my.component.js

import {BaseComponent} from './path/to/base.component'
import temmplate from './path/to/my-component-template.html';

export class MyComponent extends BaseComponent {

    constructor() {
        super(temmplate); // pass the template to super constructor
    }

    /** onInit is executed after template has been loaded */
    onInit(){
        this.mapActions();
    }

    mapActions(){
        document.getElementById('my-btn').addEventListener('click', ()=> this.myMethod() );
        //or using JQuery $('#my-btn').click( ()=> this.myMethod() );
    }
    
    myMethod(){
        document.getElementById('my-text').innerHTML = 'Hello World!';
        //or using JQuery $('#my-text').text( 'Hello World!' );
    }
}

Then register MyComponent in the file

//app.components.js
export class AppComponents {
    static getComponents() {
        return [
            {
                name:'app-my-component',
                class: MyComponent
            }
        ]
    }
}

Use a component in another component template

If you want to use a component on another component template, just add the component tag on the parent component template

<!-- parent-template.html -->
<div>
    <button id="my-btn"> Action from component </button>
</div>
<div>
    <!-- THE TEMPLATE OF MyCustomComponent WILL BE RENDERED HERE -->
    <app-my-component></app-my-component>
</div>

Communication between Components

Communication between native web components are made via events

//parent.component.js
export class ParentComponent extends BaseComponent{
    constructor() {
        super(temmplate); 
    }

    /** onInit is executed after template has been loaded */
    onInit(){
        this.mapActions();
    }

    mapActions() {
              
        //listem for event 'event-name-listened-by-parent-component'
        const childComponentElement = document.getElementById('child-component-id-on-parent-template');
        childComponentElement.addEventListener('event-name-listened-by-parent-component' , (event)=> {
            const eventData = event.detail; 
            console.log(eventData.message); //print hello world
        });
        
        //send event to child component
        this.sedToElementById('child-component-id-on-parent-template', 'event-name-listened-by-child-component', {message:'hello'} );
    }
}

//child.component.js
export class ChildComponent extends BaseComponent {
    constructor() {
        super(temmplate); 
    }

    /** onInit is executed after template has been loaded */
    onInit(){
        this.mapActions();
    }

    mapActions() {
        //listem for event 'event-name-listened-by-child-component'
        this.addEventListener('event-name-listened-by-child-component' , (event)=>{
            const eventData = event.detail;
            console.log(eventData.message); // print hello
            eventData.message += ' world';
            
            //send data to parent component
            this.send( 'event-name-listened-by-parent-component', eventData );
        });
    }
}

Loop a component to show some data

To make easier declare properties on loop template, do not extends BaseComponent

On component constructor set your template

//loop-item.component.js

import template from './path/to/template.html';
export class LoopItemComponent extends HTMLElement{
    constructor(){
        super();
        this.innerHTML = template;
    }

    connectedCallback() {
        // do something you want when the component has been attached to DOM
    }
    
    /**
     * create a instance of the compoent with them properties filled
     * @param {{id:number, description: string}} itemData 
     * @returns {ListItemComponent}
     */
    static build(itemData){
        const item = new ListItemComponent();
        item.setInfo(itemData);
        return item;
    }

    setInfo(itemData){        
        //let's suppose in your template you have the labels
        //<label id="item_id_label"> </label>
        //<label id="description_label"> </label>
        
        this.querySelector('#item_id_label').innerHTML = itemData.id;
        this.querySelector('#description_label').innerHTML  = itemData.description;
        
        //the info needed to show on your component will be set
    }
}

And to use the LoopItemComponent you can do the following

//list.component.js
import { BaseComponent } from "./path/to/base.component";
import { LoopItemComponent } from "./path/to/loop-item.component";

export class LoopItemComponent extends BaseComponent {
    constructor() {
        super('list');
        this.itens = [
            {
                id: 1,
                description: "test item 1"
            },
            {
                id: 2,
                description: "test item 2"
            },
            {
                id: 3,
                description: "test item 3"
            }
        ]
    }

    onInit() {
        this.itens.forEach( item => this.appendItem(item) );
    }

    appendItem( itemData ) {
        // data-container is the id of the element that will hold the ListItemComponent elements
        $('#data-container').append( ListItemComponent.build(itemData) );
    }
}

Services

Services are just pure ES6 JavaScript classes that holds some reusable logic

//my.service.js
export class MyService {
    someMethod() {
        const someVariable = // some logic
        return someVariable;
    }
}

//my.component.js

import {MyService} from './path/to/my.service';

export class MyComponent extends BaseComponent {

    constructor() {
        super('my-component-template'); 
        this.myService = new MyService();
    }

    onInit(){
        this.mapActions();
    }

    mapActions(){
        $('#my-btn').click( ()=> this.myMethod() );
    }
    
    myMethod(){
        const text = this.myService.someMethod();
        $('#my-text').text( text );
    }
}

Form validation

To make easy validate forms dynamically, use the FormValidationService

If you have a form like that

<!-- my-component-template.html -->
<form id="my-form">
    <div class="form-group">
        <label> Field 1: </label>
        <input app-input app-required type="text" id="field-1" class="form-control"/>
    </div>
    <div class="form-group">
        <label> Field 2: </label>
        <input app-input app-max-size="5" type="text" id="field-2" class="form-control"/>
    </div>
    <div class="form-controll">
        <button type="button" id="my-btn" > send </button>
    </div>
</form>

Then you can validate your form easely like that :

//my.component.js

import {FormValidationService} from './path/to/core/form-validation.service';

export class MyComponent extends BaseComponent {

    constructor() {
        super('my-template'); 
        this.formValidationService = new FormValidationService();
    }

    onInit(){
        this.mapActions();
    }

    mapActions(){
        this.formValidationService.whatchInputs('my-form');//trigger input validation on keyup
        $('#my-btn').click( ()=> this.validateForm() );
    }
    
    validateForm() {
        
        if(!this.formValidationService.isFormValid('my-form')) {
            console.log('FORM IS INVALID');
            // if that happens, invalid fields will be marked as red
        }
    }
}

IMPORTANT! : just fields with the attribute "app-input" will be validated

IMPORTANT! : just fields with some validator attribute like "app-required" will be validated. Don't worry, we will see more about validators on next section

IMPORTANT! : to trigger form inputs validatin on blur event just call the method watchInputs like follow

//my.component.js

import {FormValidationService} from './path/to/core/form-validation.service';

export class MyComponent extends BaseComponent {

    constructor() {
        super('my-template'); 
        this.formValidationService = new FormValidationService();
    }

    onInit(){
        this.mapActions();
    }

    mapActions(){
        //trigger input validation when input blur event is triggered
        this.formValidationService.whatchInputs('my-form');
        
        $('#my-btn').click( ()=> this.validateForm() );
    }
    
    validateForm() {
        
        if(!this.formValidationService.isFormValid('my-form')) {
            console.log('FORM IS INVALID');
            // if that happens, invalid fields will be marked as red
        }
    }
}

Validators

Validators are classes that hold some input validation logic and returns a object with a flag that say if the input is valid or not and a feedback message

//max-size.validator.js
export class MaxSizeValidator {

    /**
     * validate if the given value is lower
     * @param { string } value 
     * @param { number } validator 
     * @returns { isValid: boolean, message: string }
     */
    static validate( value , validator ) {
        if(value.length < validator){
            return { 
                isValid: true, 
                message: ''
            }
        } 

        return {
            isValid: false,
            message: 'values must be lower than ' + validator
        }
    }
}

To register your custom validator, create a ES6 class like the MaxSizeValidator and register it on ValidatorRepository

//validator.repository.js
import { MaxSizeValidator } from "./max-size.validator";
import { MyCustomValidator } from "./path/to/my-custom.validator";

export class ValidatorRepository {
    /**
     * @returns { Map< string, { validate: (value:string, validator:string) => { isValid: boolean, message: string }} > }
     */
    static getValidators() {
        const validators = new Map();
        validators.set('app-max-size', MaxSizeValidator);
        
        // app-my-validator is like you can call the validator on a form input
        // <input app-input app-my-validator />
        validators.set('app-my-validator', MyCustomValidator);
        return validators;
    }
}

IMPORTANT! : your custom validator must have the method validate that receives 2 params, first the input value, second the validator value

Communication with backend or external API via HTTP calls

To communicate with with backend or some external API via HTTP calls just use the HttpService

//my.component.js

import {HttpService} from './path/to/core/http.service';

export class MyComponent extends BaseComponent {

    constructor() {
        super('my-template'); 
        this.http = new HttpService();
    }

    onInit(){
        this.someAsyncMethod();
    }
    
    // IMPORTANT: await just works on async methods
    async someAsyncMethod() {
    
        //use await to avoid then chais
        //data
        const responseData =  await this.http.get('some-api-url');
                
    }
}

About

Custom SPA builded with pure JavaScript using Webcomponents

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published