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

cyclejs-component

v1.5.4

Published

Simple library for making clean Cycle.js components

Downloads

18

Readme

cyclejs-component

Simple library for making clean Cycle.js components

Cycle.js

Cycle.js is a functional reactive coding framework that asks 'what if the user was a function?'

It is worth reading the summary on the Cycle.js homepage, but essentially Cycle.js allows you to write simple, concise, extensible, and testable code.

cyclejs-component

The cyclejs-component library makes building Cycle.js apps and components much easier by handling all of the most complex stream plumbing, and provides a minimally opinionated structure to component code while maintaining full forwards and backwards compatibility with all Cycle.js components whether built with or without cyclejs-component.

Basics

Cyclejs-component provides only 3 functions:

  • component() - Builds a Cycle.js component that takes 'sources' and returns 'sinks'
  • collection() - Creates an auto-expanding and contracting list of components
  • switchable() - Creates a new component that 'swtiches' between a list of provided components

The component() function has several possible parameters, all of which are optional, and the most useful of which are:

  • name - A name for the component that is displayed in debug messages
  • view - A function that receives the current state and returns virtual dom
  • model - An object mapping action names to reducers or commands to be sent to sinks/drivers
  • intent - A function that receives Cycle.js souces and returns an object mapping action names to streams/observables that trigger when that action should 'fire'
  • initialState - An object representing the initial state for the component

Prerequisites

The only prerequisite to use cyclejs-component is Cycle.js itself. See the Cycle.js documentation for a full description, but the easiest way to get started is to install the create-cycle-app npm package:

npm install --global create-cycle-app

Then create a new Cycle.js app with the following command:

create-cycle-app my-awesome-app

Install cyclejs-component, and in almost all cases you will want to use state and interact with the DOM, in which case you need to install the @cycle/state and @cycle/dom packages:

cd my-awesome-app
npm install cyclejs-component @cycle/state @cycle/dom

The create-cycle-app command creates a minimal basic file structure:

  • index.js - Initializes the application
  • src/app.js(x) - Root component
  • public/index.html - HTML page that will host the application

To use the state and DOM drivers, update index.js to the following:

import { run } from '@cycle/run'
import { makeDOMDriver } from '@cycle/dom'
import { withState } from '@cycle/state'
import App from './app'

const main = withState(App, "STATE")

const drivers = {
  DOM: makeDOMDriver('#root')
}

run(main, drivers)

Now you're all set to create components! If you used create-cycle-app then you can start a WebPack dev server that watches for file changes with:

npm start

Basic Examples

Hello World

The most basic (and not very useful) component

import { component } from 'cyclejs-component'

export default component({
  view: () => <h1>Hello World!</h1>
})

Using state (basic)

If the @cycle/state driver is installed and withState() was added to the Cycle.js initialization in index.js then your component will automatically have state. The most basic way to set component state is with the 'initialState' parameter of component().

import { component } from 'cyclejs-component'

export default component({
  initialState: { who: 'World!' },
  view: ({ state }) => <h1>Hello { state.who }</h1>
})

DOM Events

To make components capable of responding to users interacting with the DOM, you will need to add the 'model' and 'intent' parameters.

The 'model' parameter is an object that maps 'action' names to what should be done when that action happens.

The 'intent' parameter is a function that takes Cycle.js 'sources' and returns an object mapping 'action' names to streams/observables which fire/emit when that action should occur.

This sounds more complicated than it is... basically the 'model' answers what can/should happen, and the 'intent' answers when those things will happen.

To illustrate, here's a basic counter that increments when the user clicks anywhere in the page:

import { component } from 'cyclejs-component'

export default component({
  // initialize the count to 0
  initialState: { count: 0 },
  model: {
    // when the 'INCREMENT' action happens, run this 'reducer' function
    // which takes the current state and returns the updated state,
    // in this case incrementing the count by 1
    INCREMENT: (state) => {
      return { count: state.count + 1 }
    }
  },
  // the 'sources' passed to intent() is an object containing an entry for each Cycle.js 'driver' passed to run() in index.js
  // the DOM source allows you to select DOM elements by any valid CSS selector, and listen for any DOM events
  // because we map document click events to the 'INCREMENT' action, it will cause the 'INCREMENT' action in 'model' to fire
  // whenever the document is clicked
  intent: (sources) => {
    return {
      INCREMENT: sources.DOM.select('document').events('click')
    }
  },
  // every time the state is changed, the view will automatically be efficiently rerendered (only DOM elements that have changed will be impacted)
  view: ({ state }) => <h1>Current Count: { state.count }</h1>
})

DOM Events (part 2)

Now let's improve our Hello World app with 2-way binding on an input field

import { component } from 'cyclejs-component'

export default component({
  // initial name
  initialState: { name: 'World!' },
  model: {
    // update the name in the state whenever the 'CHANGE_NAME' action is triggered
    // this time we use the 2nd parameter of the reducer function which gets the value passed
    // by the stream that triggered the action
    CHANGE_NAME: (state, data) => {
      return { name: data }
    }
  },
  // it's usually more convenient to use destructuring to 'get' the individual sources you need, like DOM in this case
  intent: ({ DOM }) => {
    return {
      // select the input DOM element using it's class name
      // then map changes to the value ('input' event) to extract the value
      // that value will then be passed to the 2nd parameter of reducers in 'model'
      CHANGE_NAME: DOM.select('.name').events('input').map(e => e.target.value)
    }
  },
  view: ({ state }) => {
    return (
      <div>
        <h1>Hello { state.name }</h1>
        {/* set the 'value' of the input to the current state */}
        <input className="name" value={ state.name } />
      </div>
    )
  }
})

Multiple Actions

Now let's improve the counter app with increment and decrement buttons as well as an input field to set the count to any value

import { component } from 'cyclejs-component'

// import the xtream observable library so we can do some stream operations
import xs from 'xstream'

export default component({
  initialState: { count: 0 },
  model: {
    // add the value passed from the stream that triggered the action to the current count
    // this will either be 1 or -1, so will increment or decrement the count accordingly
    INCREMENT: (state, data) => ({ count: state.count + data }),
    SET_COUNT: (state, data) => ({ count: parseInt(data || 0) })
  },
  intent: ({ DOM }) => {
    // rather than pass streams directly to the actions, it is sometimes helpful
    // to collect them in variables first
    // it is convention (but not required) to name variables containing streams with a trailing '$'
    // the 'mapTo' function causes the stream to emit the specified value whenever the stream fires
    // so the increment$ stream will emit a '1' and the decrement$ stream a '-1' whenever their
    // respective buttons are pressed, and as usual those values will be passed to the 2nd parameter
    // of the reducer functions in the 'model'
    const increment$ = DOM.select('.increment').events('click').mapTo(1)
    const decrement$ = DOM.select('.decrement').events('click').mapTo(-1)
    const setCount$  = DOM.select('.number').events('input').map(e => e.target.value)

    return {
      // the 'merge' function merges the events from all streams passed to it
      // this causes the 'INCREMENT' action to fire when either the increment$ or decrement$
      // streams fire, and will pass the value that the stream emeits (1 or -1 in this case)
      INCREMENT: xs.merge(increment$, decrement$),
      SET_COUNT: setCount$
    }
  },
  view: ({ state }) => {
    return (
      <div>
        <h1>Current Count: { state.count }</h1>
        <input type="button" className="increment" value="+" />
        <input type="button" className="decrement" value="-" />
        <input className="number" value={ state.count } />
      </div>
    )
  }
})