npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

vesseljs

v1.0.2-pre

Published

Client side JS framework for building solid MVC apps

Downloads

6

Readme

Vessel Framework

Vessel.js is a work in progress!

Vessel is javascript (Typescript) client side framework. Which structures your application in a solid MVC providing tools like Virtual DOM, Dependency Injection, Services, Routing or an Event Dispatcher.

Documentation: v1.0 (work in progress)

Download and Installation

Packages installation via NPM (you'll need to configure TypeScript, Dev and Compiling tools by your own)

npm install vesseljs

Download the ready-to-go boilerplate (recommended) (You won't need to configure anything)

Vessel (v1.0.0-pre) Boilerplate Download

Then, run in the boilerplate directory

npm install

That's it.

Usage

Now you can import the vessel classes. Type npm run serve or gulp serve —if you have gulp globally installed— to start up a developing server and watch files for changes.

Note: vessel-cli is coming!

Quick Overview

Models

Models are schemes of your data. We use @attr decorator to tell Vessel what properties are the attributes of our model, @identifier will set the property 'id' to be the identifier of the model. Also we can define an optional bridge service (see example below) and @validate the setters.

import { Model as BaseModel } from '@vessel/core';
import { attr, identifier, validate } from '@vessel/decorators';
import { ModelInterface } from '@vessel/types';

export class TodoModel extends BaseModel implements ModelInterface {

    @identifier
    @attr
    public id;

    @attr
    public author;

    @attr
    public body;

    @attr
    public date;

    protected bridge = 'service.todo';

    getId() {
        return this.attr.id;
    }

    setId(value) {
        this.attr.id = value;

        return this
    }


    getAuthor() {
        return this.attr.author;
    }

    @validate(
        function validateAuthor(value) {
            if (!(value.length >= 3 && value.length <= 20) ) {
                console.warn("Author length must be greater " +
                    "than 3 characters or less than 20.");
                return false;
            }
            return true;
        }
    )
    setAuthor(value) {
        this.attr.author = value;

        return this;
    }

    getBody() {
        return this.attr.body;
    }

    @validate(
        function validateBody(value) {
            if (!(value.length >= 0 && value.length <= 120) ) {
                throw TypeError("Body length must be less " +
                    "than 120 characters and must not be empty.");
            }
            return true;
        }
    )
    setBody(value) {
        this.attr.body = value;

        return this;
    }
}

Collections

Collections are just sets of models in memory. Just like models, a bridge service can be defined so you can use .fetch() and .create() methods. Also, collections have an API available for managing its models.

  • find({'id': 3}): returns the first ocurrence based on an attribute(s) search,
  • findAll({ 'name': 'test'}): returns all the ocurrences based on an attribute(s) search,
  • pull('name'): plucks the attribute 'name' from every model and returns an array with the values.

You can define a comparator so when new models are added they will keep the order.

import { Collection as BaseCollection } from '@vessel/core';
import { TodoModel } from '../model/TodoModel';

export class TodoCollection extends BaseCollection {

    /**
     *  Service Bridge (module name) to  be used.
     */
    protected bridge = 'service.todo';
    
    /**
     *  Collection Model.
     */
    public model = TodoModel;
    
    /**
     *  Insert models ordered by name attribute.
     */
    public compare = 'name';
}

Services

Your application is full of classes which perform useful actions, such as retrieving or processing data. That's the Service's component purpose. Services have two main objectives:

  • Connect your application with external data —like Storage or a Rest Api— RemoteServices.
  • Perform the same actions among the controllers —for example, processing data— Services.

You can create your own RemoteServices, so you have full control over your requests or you can code your own Bridge Services. This services are a special kind of pre-configured RemoteServices, which allows you to use collection.fetch(), model.fetch(), model.save(), model.remove() methods. For example, we're gonna use the HttpBridge so we can use our remote methods.

Here, we define what is the endpoint and how we're serializating and retrieving the information as well. E.g.: the create method is telling the bridge to set the model id when it is saved and to return this model so when we perform a model.save(), the framework will execute a request as follows: POST /model/{id}. Then it will set the model id to be the id which the server just created and given to us. At this point, you may notice that, what the create() method returns is exactly what the model.save() will return as a promise (see a controller example below).

import { HttpBridge } from '@vessel/core';

export class TodoService extends HttpBridge {

    protected endPoint = '/weather';

    protected getResponse(response) {
        return JSON.parse(response);
    }

    public create(jsonResponse, model) {
        model.setId(jsonResponse.id);
        return model;
    }

    public read(jsonResponse, obj) {
        return jsonResponse;
    }

    public update(jsonResponse, model) {
        return model;
    }

    public destroy(jsonResponse) {
        return jsonResponse.id;
    }
}

When a method crud is used by both collection and model such as read, Vessel will automatically perform the requests accordingly: GET /collection or GET /model/{id}. E.g.: todoCollection.fetch() will trigger a GET /todos request, todoModel.fetch() will produce a GET /todos/3 assuming TodoModel exists and has the id 3.

If your application needs to return different results depending on what the obj is: a model or a collection you can use isModel(obj) or isCollection(obj) so you can define what will the collection.fetch() and model.fetch() retrieve using the same bridge service.

Dependency Injection

Vessel provides a dependency injector so you don't have to worry about instantiating classes or booting up your application when you need the same instance for multiple purposes (Services, Controllers, Views, Collections, etc.). First, you have to register your modules, this is an example of a modules.ts file:

import { TodoController } from '../controller/TodoController';

import { TodoModel } from '../model/TodoModel';

import { TodoCollection } from '../collection/TodoCollection';
import { ThirdCollection } from '../collection/ThirdCollection';
import { TestCollection } from '../collection/TestCollection';
import { FourthCollection } from '../collection/FourthCollection';
import { FifthCollection } from '../collection/FifthCollection';

import { TodoView } from '../view/TodoView';

import { TodoService } from '../service/TodoService';

export const modules = {

    models: {
        'model.todo': TodoModel,
    },

    controllers: {
        'controller.todo': TodoController
    },

    collections: {
        'collection.todo': TodoCollection,
        'collection.test': TestCollection,
        'collection.third': ThirdCollection,
        'collection.fourth': FourthCollection,
        'collection.fifth': FifthCollection
    },

    views: {
        'view.todo': TodoView
    },

    services: {
        'service.todo': TodoService
    }

};

This file has to be imported from the main.ts of your application.

import { RouterBoot } from '@vessel/router';
import { VirtualDOMBoot } from '@vessel/dom';

import { app as appConfig } from './config/app';
import { modules } from './config/modules';
import { BaseApp } from '@vessel/core';
import { bootable } from '@vessel/decorators';

@bootable
class App extends BaseApp {

    public registerConfig() {
        return appConfig;
    }

    public registerModules() {
        return modules;
    }

    public registerPackages() {
        return [
            new RouterBoot(),
            new VirtualDOMBoot()
        ];
    }

    public getGlobalName() {
        return '$App';
    }

}

Notice how the modules are registered within the registerModules() method. The Vessel's Kernel will register this configuration and your modules will be ready to go! Once you have registered your modules, you can use the injector.

get() Method

The get method is included in our Collection, View, Model, Service and Controller classes and it is a container.get() alias. This method will return a single instance of the given module name and resolve its dependencies recursively. When a module is resolved, all the dependencies specified by the @get Decorator are injected to its instance. You will be warned if a circular dependency is detected.

   // import [...]
   
   class MyController extends BaseController {
        
        method() {
            // Resolve the module TodoCollection and its dependencies
            // recursively.
            let collection = this.get('todo.collection'); 
        }
   }

@get() Decorator

The @get() decorator can be used to add dependencies to a class. Note that this will not actually inject any dependencies. This dependencies are resolved when the class, which we're applying our decorator to, is loaded by a .get() method or the class it's a dependency of a class which is being loaded by the .get() method.

MyCollection.ts (module name: todo.collection)

   // import [...]
   
   class MyCollection extends BaseCollection {
        
       @get('testService') 
       public testService;
       
   }

MyController.ts (module name: todo.controller)

   // import [...]
   
   class MyController extends BaseController {
        
        method() {
            // Collection will be resolved, as well as its dependency testService.
            let collection = this.get('todo.collection');
        }
   }

AnotherController.ts (module name: todo.controller_two)

   // import [...]
   
   class MyController extends BaseController {
        
        method() {
            // Same collection as the MyController one.
            let collection = this.get('todo.collection');
        }
   }

Limitations:

  • You can't use a @get dependency within a constructor, as it is injected right after the instantiation. That said, this is wrong:
   // import [...]
   
   class MyController extends BaseController {
        
        @get('collection')
        public myCollection;
        
        constructor() {
            let collection = this.myCollection // collection will be undefined.
        }
        
        method() {
            let collection = this.myCollection // collection will be right.
        }
   }

But you can always use this.get(), this is a working snippet for the code right above:

   // import [...]
   
   class MyController extends BaseController {
        public myCollection;
        
        constructor() {
            this.myCollection = this.get('collection') // myCollection will be right.
        }
        
        method() {
            let collection = this.myCollection // collection will be right.
        }
   }
  • You can't use a @get dependency within classes that will have more than one instance at the same time, for example a TodoModel will be instantiated several times at runtime so dependency injection have no sense and they're not gonna be resolved since you're not starting a model with the container.get() method. However, this.get() can be used within models so, for example, you can get a service (which is not recommended, models should be used only for data schemes).

Controllers

Controllers manage the user-app interaction. A controller consists of action methods such as editTodo, viewTodo, storeTodo, indexTodo, removeTodo, etc. Each action is associated with a route name and an optional route path. In most cases, controllers use the collections, services, etc. to retrieve, fetch, proccess... data and then, render the resulting data.

A controller can render a view with its module name with the render() method passing the data to be render or render other routes by its name with renderRoute(). When an action is triggered by its route path the route parameters are passed in to the action as the first parameters.

import { Controller as BaseController } from '@vessel/core';

import { route } from '@vessel/router';
import { get } from '@vessel/decorators';

import { TodoModel } from '../model/TodoModel';

export class TodoController extends BaseController {

    @get('collection.todo')
    public todoCollection;

    @route('todo_edit', '/edit/:id')
    public async editTodo(id, title, body) {
        let updatedTodo,
            todo = this.todoCollection.find({'id': id});
        
        todo.setTitle(title)
            .setBody(body);
            
        updatedTodo = await todo.save();

        return this.renderRoute('todo_view', updatedTodo.id);
    }
    
    @route('todo_view', '/todo/:id')
    public viewTodo(id) {
        let todo = this.todoCollection.find({'id': id});
        return this.render('view.todo', { id: todo.id, title: todo.title, body: todo.body });
    }
}

Views

Views are not aware of the rest of your application. Vessel uses a basic built-in Virtual DOM with an abstraction layer, which is quite similar to the vanilla DOM (a template engine may be used in the future). Views have a status object when they're being rendered, which is used for rendering the data from the controllers. Also, views have a renderRoute() method so they can call routes by name which will trigger controller actions.

import { View } from '@vessel/core';
import { VirtualDOM } from '@vessel/dom';

export class TodoView extends View {

    public parent = '#todo-root';

    public constructor() {
        super();
        this.onRefresh = this.onRefresh.bind(this);
    }

    public onRefresh() {
        this.renderRoute('todo_edit', {id: ++this.state.id});
    }

    public render() {
        let div, p, i, button, input,
            self = this;

        div = this.create('div')
            .set({
                'class' : 'todo-class'
            })
            .css({
                'color': 'red'
            });

        p = this.create('p')
            .text("I'm the todo id ")
            .appendTo(div);

        i = this.create('i')
            .text(this.state.id)
            .appendTo(p);

        input = this.create('input')
            .set({
                'id': 'input'
            })
            .appendTo(div);

        button = this.create('button')
            .text("Refresh")
            .click(self.onRefresh)
            .appendTo(div);

        return div;
    }

}

As it is, the Virtual DOM will re-render only the needed nodes.

virtual_dom