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

sagan

v1.1.1

Published

A typing and state management system for JavaScript apps

Downloads

15

Readme

Sagan

Sagan is a type checking state container for JavaScript apps.

It provides consistant state behavior, type checking for models, universal (client, server) applicability, and easy testing and time traveling debugging.

You can use Sagan together with React or with any other view library.

Influences

Sagan is based on learnings from Redux/Rematch and Backbone.js Even if you haven't used either, Sagan only takes a few minutes to get started with.

Installation

npm install --save sagan

The Sagan source code is written in ES2015 but we precompile both CommonJS and UMD builds to ES5 so they work in any modern browser. You don't need to use Babel or a module bundler to get started with Redux.

Most commonly, people consume Sagan as a collection of CommonJS modules. These modules are what you get when you import redux in a Webpack, Browserify, or a Node environment.

If you don't use a module bundler, it's also fine. The sagan npm package includes precompiled production and development UMD builds in the dist folder.

Complementary Packages

Most likely, you'll also need the Sagan bindinds and the developer tools.

npm install --save react-sagan
npm install --save-dev redux-devtools

Overview

Sagan borrows the idea of state immutibility from React and combines it with the power of typed models and collections that are very similar in application to Backbone.js or Ampersand.js Additionaly, Sagan borrows the concept of reducers from React for those that desire to follow that paradigm, but is is not actually necessary for updating state as we shall see in our examples.

Getting Started

Models

Models bring together state, derived state, and reducers in one place. The model is also responsible for type checking your data.

model.js

import { extendModel } from 'sagan'

const User = extendModel({
    props: {
        firstName: {
            type: 'string',
            required: true
        },
        lastName: 'string',
        address: {
            type: 'object',
            required: false,
            props: {
                street: 'string',
                city: 'string'
            }
        }
    },
    derived: {
        fullName: {
            deps: ['firstName', 'lastName'],
            fn: function () {
                return this.state.firstName + ' ' + this.state.lastName
            }
        }
    },
    reducers: {
        setFirstName: function(state, payload) {
            return {
                ...state,
                firstName: payload
            }
        },
        setLastName: function(state, payload) {
            return {
                ...state,
                lastName: payload
            }
        },
        setName: function(state, payload) {
            return {
                ...state,
                ...payload
            }
        }
    }
})

export default User

Props

The props object is where you define type data for your model. Types may be defined by either passing the type as a string, or by passing a type object. They type object allows you to specify additional requirements and well as create nested typed objects.

props: {
        firstName: {
            type: 'string',
            required: true
        },
        lastName: 'string',
        address: {
            type: 'object',
            required: false,
            props: {
                street: 'string',
                city: 'string'
            }
        },
        contacts: {
            type: 'array',
            required: false,
            elements: {
                street: 'string',
                city: 'string'
            }
        },
        phoneNumbers: {
            type: 'array',
            required: false,
            elements: 'string'
        }
    },
Type Object
{
    type: ['string', 'boolean', 'number', 'object', 'array', 'any'],
    required: [boolean] (optional),
    props: [type object] (optional),
    elements: [type object, 'string', 'boolean', 'number', 'array', 'any'] (optional)
}

type: Objects may recieve a type of a string, boolean, number, object, array, or any.

requred: (optional) Props may be specified as required. The default is false. Props that are typed directly without a type object default to false

props: (optional) The props option is reserved to props that are typed as an object. This option allows you to type nested props.

elements: (optional) The elements option is reserved to props that are typed as an array. This option allows you to type array elements.

Derived Props

Sagan allows you to specify derived props. These are updated when their prop dependencies update.

derived: {
        fullName: {
            deps: ['firstName', 'lastName'],
            fn: function () {
                return this.state.firstName + ' ' + this.state.lastName
            }
        }
    },

deps: Specify the props this derived prop is dependent on. This prop will throw an error if a dependent prop is missing.

fn: The return function that generates the derived prop.

Reducers

Similarily to Redux, you may specify reducers for updating state for your model. However, it is not necessary to do so as a helper update function is exposed that can be used in the majority of cases. We will go over this in the Updating State section.

reducers: {
    setFirstName: function(state, payload) {
        return {
            ...state,
            firstName: payload
        }
    }
}

Collections

Collections are objects that allow you to organize and type check groups of models.

import { extendCollection} from 'sagan'
import User from 'user.model'

const UserCollection = extendCollection({
    model: User,
    reducers: {
        addUser: function(state, payload) {
            return [
                ...state,
                payload
            ]
        },
        removeUser: function(state, payload) {

            const newArray = state.filter((item, i) => {
                return i !== payload
            })

            return newArray
        }
    }
})

export default UserCollection

Model

Specify the model that you would like the collection to inherit its typings from.

Reducers

Like models, collections allow you to create reducers for the collection as a whole. Depending on the use case, this may not be necesarry as the collection object exposes addItem and removeItem as helper methods.

Store - Bringing it all together

Store configures your reducers, devtools and middlewares.

index.js

import { Store, middleware } from 'sagan'
import User from 'user.model'
import UserCollection from 'user.collection'

const userInstance = new User({
    firstName: 'Trooper',
    lastName: 'TK-421',
    address: {
        street: '203 Trash Compactor Lane',
        city: 'Death Star'
    }
})

const UserCollectionInstance = new UserCollection([
    {
        firstName: 'Darth',
        lastName: 'Vader'
    },
    {
        firstName: 'Luke',
        lastName: 'Skywalker'
    }
])

const store = new Store({
    models: {
        user: userInstance,
        users: UserCollectionInstance
    },
    middlewares: [middleware.logger]
})

export default store

Middleware

You may use middleware with Sagan that pass state and action as arguments. It will be called after state has been updated. Sagan comes with an importable logger middleware by default.

export default function(state, action) {
    console.log(`${action.type} - dispatched`, state)
}

Updating State

The driving principle underlying Sagan is ease of dispatching actions and updating state. In fact, there are several ways of doing so depending on your need

Here is a basic example of dispatching a namespaced payload to a reducer. Let's break down the action type user/firstName. In this case user is the model we want to specifically dispatch the action to, and firstname is the reducer we would like trigger.

store.dispatch({type: 'user/firstName', payload: {firstName: 'Darth'}})

The same action may also be dispatched without a namespace specified. Unlike the first example. An action without a namespace will attempt to trigger the firstName reducer on all models and collections in the store. This is useful if you need to trigger updates on multiple points across the store.

store.dispatch({type: 'firstName', payload: {firstName: 'Darth'}})

Models

Models inherit an update method that must be namespaced when used. Take the following as an example. Here we are dispatching the update method to the user namespace (model). This method will attempt to merge the previous model's state with the passed payload.

store.dispatch({type: 'user:update', payload: {firstName: 'Lea', lastName: 'Organa'}})

Collections

Collections inherit addItem and removeItem as methods. They are namespaced and used in the same manner as the model methods.

The following will add a new user to the users collection.

store.dispatch({type: 'users:addItem', payload: {firstName: 'Lea', lastName: 'Organa'}})

This example will remove a user at the following index.

store.dispatch({type: 'users:removeItem', payload: 1)

Nested Collections

Dispatching an action to a collection nested within a model requires piping the collection namespace to the reducer you wish to trigger. This may be done with user defined or inherited reducers.

Note: Due to possible action/reducer recursion on nested collection models, it is currently not possible to dispatch actions beyond single layer model/collection nestings.

The following will dispatch the addAddress action to the addresses collection on the user model. [model]/[collection]|[reducer]

store.dispatch({type: 'user/addresses|addAddress', payload: {city: 'Mos Eisley'}})

This example achieves the same end result, but instead uses the inherited addItem action.

store.dispatch({type: 'user:addresses|addItem', payload: {city: 'Mos Eisley'}})

Listening to Updates

Normally you'd want to use a view binding library (e.g. Sagan Redux) rather than subscribe(). However, it can be handy to have direct acces.

store.subscribe((state) => {
    console.log(state)
})