Coder Social home page Coder Social logo

motorcyclejs / motorcyclejs Goto Github PK

View Code? Open in Web Editor NEW
105.0 11.0 4.0 1.81 MB

A statically-typed, functional and reactive framework for modern browsers

License: MIT License

JavaScript 2.13% TypeScript 97.42% Shell 0.45%
functional reactive typescript client-side architecture motorcycle

motorcyclejs's Introduction

Motorcycle.js

A statically-typed, functional and reactive framework for modern browsers

Motorcycle.js is a collection of many packages that accomplished specific needs. This repository contains all of the packages maintained by the Motorcycle core contributors. Each package has a README for more detailed information about installation and usage.

Package name Version Dependencies
@motorcycle/dom
@motorcycle/history
@motorcycle/http
@motorcycle/i18n
@motorcycle/local-storage
@motorcycle/router
@motorcycle/run
@motorcycle/session-storage

Most is an ultra-fast reactive-programming library for JavaScript with which some of our core contributors are heavily involved in. Most is the workhorse for Motorcycle, and for those who are unfamiliar, here are some helpful links to get started with Most:

For those who are interested in contributing here are some helpful links for getting started

motorcyclejs's People

Contributors

fiatjaf avatar frikki avatar mosaic-thomasklonecke avatar tylors avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

motorcyclejs's Issues

Some ideas about the future of DOM

Cheers! This issue contains brainstorming and some ideas how the "future of DOM" would look like in my eyes. These are not direct feature requests, instead I'm hoping that these topics raise some discussion that (in the end) leads to decisions useful for the entire community.

I've already discussed about this in Cycle community but Cycle's ideology and these ideas seem contradictory, hence I'm continuing here in the case that Motorcycle community shares at least some of them. ๐Ÿ˜„


Transposition

Cycle had transposition (or "embedding observables into vdom") but it was removed lately. I'm not so familiar with the reasons but in my opinion that is one of the best features one can have in vdom, and proposing that it'll be included in @motorcycle/dom because:

  • It enables blazing fast DOM updates; when something changes in vdom, only the changed vnode can be diffed and patched in O(1) time
  • It reduces boilerplate and redundancy because there is no need to combine observables before using them
  • It's backwards compatible and opt-in; you can still use O.combine with it without having any negative performance / API usage impact

Consider BmiCalculator from cycle-examples:

function view(bmi$, weightSliderDOM, heightSliderDOM) {
  return xs.combine(bmi$, weightSliderDOM, heightSliderDOM)
    .map(([bmi, weightVTree, heightVTree]) =>
      div([
        weightVTree,
        heightVTree,
        h2(`BMI is ${bmi}`)
      ])
    );
}

Could be written as:

function view(bmi$, weightSliderDOM, heightSliderDOM) {
  return div([
        weightVTree,
        heightVTree,
        h2(bmi$.map(bmi => `BMI is ${bmi}`))
      ])
}

You may also notice an interesting extra feature: view function doesn't need to know whether DOM params are observables or not. This makes e.g. unit testing easier and makes components more reusable; consider this:

function LabeledInput({title, value}) {
  return h("label", [
    title,
    h("input", {type: "text", value})
  ])
} 

Now the labeled input view doesn't need to know about the exact details of title and value, hence it can be called in many ways:

const inputA = LabeledInput({title: "Tsers", value: state.select("tsers")})
const inputB = LabeledInput({title: state.map(({foo}) => foo ? "Foo" : "Bar"), value: state.select("bar")})
const inputC = LabeledInput({title: "Const", value: "123"})

About type safety and "magic"

One might argue that embedded observables are not type safe or some "implicit magic". I don't see it that way - it just the contract of vnode. How about O.combine (combine :: [Observable<A>] => Observable<[A]>), is that any more magic? Nope, it's just a way how combine works.

When expressed with types, the signature of h is something like this:

type Prop = string | num
type Child = string | VNode
type Children = Child | [Child]
type VNode = {value: Prop?, ..., children: Children}
h :: (string, {value: Prop?, ...}?, Children?) => VNode

With transposition, the signature is a bit more complicated but still strongly typed:

type Prop = string | num | 
type ObsProp = Prop | Observable<Prop>
type Child = string | VNode
type ObsChild = Child | Observable<Child>
type Children = ObsChild | [ObsChild] | Observable<[Child]>
type VNode = {value: ObsProp?, ..., children: Children}
h :: (string, {value: ObsProp?, ...}?, Children?) => VNode

Event subscriptions

For a long time in (motor)cycle apps, DOM event streams have been obtained by using DOMSource, e.g.

const click$ = DOM.select(".foo").events("click")

This approach favors the "traditional" cycle state management where model depends on intent and it has been beneficial to get events "first":

// intent
const plus$ = DOM.select(".inc").events("click").constant(+1)
const minus$ = DOM.select(".dec").events("click").constant(-1)

// model
const sum$ = O.merge(plus$, minus$).scan(R.add, 0)

// view
const vdom$ = sum$.map(sum => ...)

This works pretty well in toy apps, but apps with more complicated state management (real apps IMO) this approach tends to enforce cumbersome and complex code. This has been (finally!) understood in Cycle community, thus libraries like onionify have been developed.

An another problem in this approach is that there must be a way to isolate events targeted to two or more sibling components. Because the events are coming directly from DOM the only way to distinguish siblings is to make 2-staged isolation: (1) assign some identity to component's root DOM node and (2) filter emitted events based on that identity.

In cycle, isolate handles this operation:

const IsolatedComp = isolate(Comp, id)

Note that in order to preserve referential transparency ("purity"), id must given to the isolation. If I've understood correctly, there won't be any isolate in Motorcycle. This means that the user must perform both of those tasks and define id manually every time when isolation is needed:

const a = Comp({...sources, DOM: DOM.select(".a")})
const b = Comp({...sources, DOM: DOM.select(".b")})

const vdom = h("div", [
  h("div.a", a.DOM),
  h("div.b", b.DOM)
])

This however, introduces (IMHO) unnecessary boilerplate and requires extra DOM nodes (or even more boilerplate with a.DOM.map(withClass)) in order to ensure isolation. In addition, events may still "leak" from child component to parent if the parent component subscribes to events by using selector matching child component nodes. This is also more error prone because class selectors must be defined in multiple places.

Monadic event subscription

I see cycle applications just as monadic functions projecting state (over time) into view (over time) and a bunch of control events/actions Main :: Behaviour<State> => [Behaviour<View>, EventStream<Action>]. How about if we modeled DOM and it's event in a similar way? How about if events could be subscribed directly from vnode instead of DOMSource?

// model
function main({onion: state}) {
  // view
  const vdom = h("div", [
    h("h1", state.value.map(name => `Hello ${name}!`),
    h("button.send", "Send me!"),
    h("input.name", {type: "text", value: state.value})
  ])
  // intent
  const click$ = vdom.events(".send", "click")
  const type$ = vdom.events(".name", "input")
  return {
    DOM: vdom,
    onion: O.merge(
      click$.map(() => () => ""),
      type$.map(e => () => e.target.value)
    )
  } 
}

This proposal of course requires that the "official" state management supports circular dependency by default (like onionify does) because it changes the dependency order from I -> M to M -> I.

But what benefits do we get by doing this this? As you might have noticed, there is no dependency to DOM when defining events, hence we don't need to perform any filtering in our parent component. And because we don't need to do filtering, we don't need to encode any filtering info into our DOM nodes. Parent nodes become extremely simple:

const a = Comp(sources)
const b = Comp(sources)

const vdom = h("div", [ a.DOM, b.DOM ])

Of course there is still a situation where events "leak" from child component to parent. I see two possible alternatives to tackle this:

  • Introduce an optional isolated function that can be used in parent component if the isolation must be ensured
const a = Comp(sources)
const b = Comp(sources)

const vdom = h("div", [ isolated(a.DOM), isolated(b.DOM) ])
  • Do not give any option to use selector when defining events; events may only be defined directly by using the vnode associated with them
 function main({onion: state}) {
  let send, name
  const vdom = h("div", [
    h("h1", state.value.map(name => `Hello ${name}!`),
    send = h("button.send", "Send me!"),
    name = h("input.name", {type: "text", value: state.value})
  ])
  // intent
  const click$ = send.events("click")
  const type$ = name.events("input")
  return {
    DOM: vdom,
    onion: O.merge(
      click$.map(() => () => ""),
      type$.map(e => () => e.target.value)
    )
  } 
}

That was my 2 cents. Looking forward to some discussion about the topic. ๐Ÿ˜„

Remove user exercises across Basic Usage examples

I just wanted to suggest replacing all instances of '// left as an user exercise' with the actual working solution in the 'Basic Usage' examples across all motorcyclejs packages.

Basic examples should act as a sanity check for beginners by providing simple but complete samples they can import and run without having to ask themselves "Am I doing it wrong? Or is there something wrong with the example?"

I do think encouraging this type of self exploration can only make for stronger developers with deeper knowledge of the framework ...but this is the wrong place, IMO.

Project status, DevTool

Can you explain current status of project?

What is @motorcyclets and why it's not in this repo?
How can I use Cycle.js DevTool with motorcycle?

Add I/O / Gateway / Effectful Component Sink Types

When defining types, such as MainSinks, we now have to explicitly tell that at sink is a Stream of something that an I/O / gateway / effectful component (a.k.a. driver) requires; for example:

interface MainSinks {
  view$: Stream<VNode>;
  localStorage$: Stream<LocalStorageCommand>;
}

It also, in turn, requires us to import { Stream } from 'most'. This seems like unnecessary boilerplate. Instead, we could let the I/O / gateway / effectful component export the sink type, which it requires, e.g., LocalStorageSink or DomSink.

Identity and Access - Collection of drivers/tools

Identity and Access is central to applications and can be abstracted to facilitate common functionality.

Given the new run() type signature, we are now able to define Components, in the same fashion as we do for our main function, for our effects function with the type signature Sinks -> Sources.

Identity and Access Components to implement

  • Register a user --- Username / Password
  • Deregister a user
  • Sign In
  • Sign Out
  • Change password
  • Get current user

Progress can be tracked on #24

how to recover from HTTP errors?

I'm having a hard time using recoverWith to replace an erroneous response stream with the same response stream, just without the error. How do you handle this with most/ the Motorcycle HTTP driver?

Text nodes under isolated tree cause crash during vdom update

Code to reproduce the issue:

See #26

Expected behavior:

UI updates correctly after slider has been moved

Actual behavior:

Crash. Probably caused because the slider's vtree is isolated, thus its root node has scope which is inherited to the children in hyperscript helper. Howerever vnode's update function blindly tries to set scope by using Element.setAttribute although text nodes (children that inherit the scope attribute in hyperscript helper) don't have one.

Versions of packages used:

"@culli/store": "latest",
"@cycle/isolate": "^1.4.0",
"@motorcycle/dom": "^7.0.6",
"@motorcycle/run": "^1.2.5",

What am I doing wrong? :P

Given the following files.

  • index.js
  • List.js
  • Slider.js

Trying to reimplement: https://github.com/milankinen/culli/tree/master/perf
for motorcycle as to have a comparison with cycle itself, but I'm failing hard with s.subscribe not being a function. The strange part is that in the kitchen sink project I'm trying things out in the store and motorcycle just work well together, I'm wondering where this example might be wrong.

Please let me know if you had a minute to reproduce to issue or have a suggestion to fix it :)

index.js

import {run} from '@motorcycle/run'
import {makeDomDriver} from '@motorcycle/dom'
import Store, {Memory} from '@culli/store'
import List, {nextId} from './List'
import { O } from '@culli/base'

const N = 10000
const sliders =
  Array.apply(0, Array(N)).map(() => ({ id: nextId(), val: Math.floor(10 + Math.random() * 80) }))

const domDriver = makeDomDriver(document.body)
const storeDriver = Store(Memory({items: sliders}))

function effects(sinks) {
  return {
    DOM: domDriver(sinks.DOM),
    Store: storeDriver(sinks.Store, O.Adapter)
  }
}

run(List, effects)

List.js

import * as O from 'most'
import Slider from './Slider'
import { h1, div, hr, button } from '@motorcycle/dom'
import { combineObj } from 'most-combineobj'
import isolate from '@cycle/isolate'

let ID = 0
export const nextId = () => ++ID

export default function main(sources) {
  const {Store} = sources
  const {dispatch, props} = model(Store)
  const {vdom, children} = view(props)
  const actions = intent(sources.DOM)

  return {
    DOM: vdom,
    Store: O.merge(dispatch(actions), children.Store)
  }

  function model({actions, value}) {
    const dispatch = actions.reduce((state, action) => {
      switch (action.type) {
        case 'ADD':
          return {items: [{id: nextId(), val: Math.floor(10 + Math.random() * 80)}, ...state.items]}
        default:
          return state
      }
    })

    return {
      dispatch,
      props: {
        items: value.select('items')
      }
    }
  }

  function view({items}) {
    const children = items.value.mapChildren(it => isolate(Slider)({...sources, Store: it}),
      {values: ['DOM'], events: ['Store']})
    const state = combineObj({ childrenDOM: children.DOM, items: items.value })
    const vdom = state.map(({ childrenDOM, items }) => div({}, [
      h1('.header', `CULLI witch cycle DOM: ${items.length}`),
      button('add', 'Add slider'),
      hr(),
      div('.sliders', childrenDOM)
    ]))

    return {vdom, children}
  }

  function intent(domSource) {
    return domSource
      .select('.add').events('click')
      .map(e => ({type: 'ADD'}))
  }
}

Slider.js

import { div, input } from '@motorcycle/dom'

export default function main({ DOM, Store, min = 0, max = 100, step = 1 }) {
  const {dispatch, props} = model(Store)
  const vdom = view(props, {min, max, step})
  const actions = intent(DOM)

  return {
    DOM: vdom,
    Store: dispatch(actions)
  }

  function model({actions, value}) {
    const dispatch = actions.reduce((state, action) => {
      switch (action.type) {
        case 'SET':
          return {...state, val: action.payload}
        default:
          return state
      }
    })

    return {
      dispatch,
      props: {
        val: value.select('val')
      }
    }
  }

  function view({val}, {min, max, step}) {
    return val.value.map(val => {
      return div({}, [
        input('.slider', { attrs: {type: 'range', min, max, step, value: val} }), ' ', val
      ])
    })
  }

  function intent(domSource) {
    return domSource
      .select('.slider').events('input')
      .map(e => ({type: 'SET', payload: e.target.value}))
  }
}

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.