Giter Site home page Giter Site logo

barneycarroll / mithril-machine-tools Goto Github PK

View Code? Open in Web Editor NEW
30.0 6.0 3.0 284 KB

Putting the hype back in hyperscript, the OM back in virtual DOM; A bag of tricks for Mithril.

License: MIT License

JavaScript 92.07% HTML 7.93%
mithril hyperscript virtual-dom

mithril-machine-tools's Introduction

Mithril Machine Tools

Putting the hype back in hyperscript, the OM back in virtual DOM; A bag of tricks for Mithril.

Components are a popular mainstream abstraction, but the true power of component composition is largely unexplored. Mithril Machine Tools is a pragmatic demonstration of what is possible with components that seek to expose โ€” rather than enclose โ€” the power of Mithrils hyperscript & virtual DOM interfaces. Use these tools as aids in application design, or as conceptual aids in building your own abstractions!

import {
  // ๐Ÿ‘‡ Components
  createContext, Inline, Liminal, Mobile, Promiser, Static,

  viewOf, indexOf, domOf, getSet,
  // ๐Ÿ‘† Utilities
} 
  from 'mithril-machine-tools'

Components

createContext

createContext emulates Reacts context API, using the virtual DOM hierarchy as a data transport mechanism in a manner similar to CSS properties vis-ร -vis the DOM. Values set by a Provider component can be retrieved by a corresponding Receiver component anywhere in its subtree.

import {createContext} from 'mithril-machine-tools'

const {Provider, Consumer} = createContext()

m.mount(document.body, {
  view : () =>
    m(Provider, {value: state}, 
      m(Layout), // Nothing is passed in to Layout
    ),
})

// In fact, layout doesn't know anything about context
function Layout(){
  return {
    view : () => [
      m(Header),
      m(Reader),
      m(Footer),
    ],
  }
}

function Reader(){
  return {
    view : () =>
      // Consumer retrieves the value set by its virtual DOM ancestry
      m(Consumer, value =>
        m('code', 'value === state :', value === state), 
      ),
  }
}

Inline

The Inline component takes a component expression as its input: This allows you to describe stateful behaviour inline in the virtual DOM tree itself, affording all the benefits of localised isolation without the restrictive indirection.

import {Inline} from 'mithril-machine-tools'

m.mount(document.body, function A(){
  let a = 0
  
  return {
    view: () => [
      m('p', {
        onclick: () => { a++ },
      }, 'a is ', a),

      m(Inline, function B(){
        let b = 0

        return {
          view: () => [
            m('p', {
              onclick: () => { b++ },
            }, 'b is ', b),

            m('p', 'a * b  is ', a * b),
          ],
        }
      })
    ],
  }
})

Mobile

In Mithril, "Keys are a mechanism that allows re-ordering DOM elements within a NodeList". Mobile exposes a Unit component which provided with a persistent key can move anywhere within the Mobile view.

import {Mobile} from 'mithril-machine-tools'

m.mount(document.body, {
  view: () =>
    m(Mobile, Unit => 
      ['To do', 'Doing', 'Done'].map(status => 
        m('.Status',
          m('h2', status),

          issues
            .filter(issue => issue.status === status)
            .map(issue =>
              m(Unit, {key: issue.id},
                m(Issue, {issue}),
              ),
            ),
        ),
      ),
    ),
})

Promiser

Promiser consumes a promise and exposes a comprehensive state object for that promise, redrawing when it settles. This allows convenient pending, error & success condition feedback for any asynchronous operation without bloating your application model.

import {Promiser} from 'mithril-machine-tools'

m.mount(document.body, function Search(){
  let request

  return {
    view: () => [
      m('input[type=search]', {oninput: e => {
        request = m.request('/search?query=' + e.target.value)
      }}),

        !request 
      ? 
        m('p', '๐Ÿ‘† Use the field above to search!')
      :
        m(Promiser, {promise: request},
          ({value, pending, resolved}) => [
            pending && m(LoadingIndicator),

            resolved && m(Results, {value}),
          ],
        ),
    ],
  }
})

Static

Static allows you to mark a section of view that has no dynamic requirement, & consequently never needs to recompute; it exposes a Live component which is used to opt back in to computation lower down the tree. This can be useful to distinguish between voluminous UI whose purpose is purely structural & cosmetic, & stateful, dynamic UI within it.

import {Static} from 'mithril-machine-tools'

m.mount(document.body, {
  view: () =>
    m(Static, Live => [
      m(Nav),

      m(Header,
        m(Live, m('h1', title)),
      ),

      m(PageLayout,
        m(Live, m(Form)),
      ),
    ],
})

Liminal

Liminal is an effects component which applies CSS classes to the underlying DOM to reflect lifecycle, listens for any CSS transitions or animations triggered by the application of these classes, and defers removal until these effects have resolved. The component accepts any of the attributes {base, entry, exit, absent, present} to determine what classes to apply, and an optional blocking attribute which if true, ensures that entry effects complete before exit effects are triggered; the class properties can be space-separated strings containing multiple classes. Liminal must have a singular element child.

import {Liminal} from 'mithril-machine-tools'

m.route(document.body, '/page/1', {
  '/page/:index': {
    render: ({attrs: {index}}) =>
      m(Liminal, {
        key: index,
        base   : 'base',
        entry  : 'entry',
        exit   : 'exit',
        absent : 'absent',
        present: 'present',
      },
        m('.Page', 
          m('.Menu'),
        ),
      ),
  },
})
.Page.base {
  transition: opacity 400ms ease-in-out;
}

.Page.absent {
  opacity: 0;
}

.Page.present {
  opacity: 1;
}

/* CSS selectors can qualify effects based on ancestry */
.Page.present .Menu {
  animation: slideIn 600ms ease-in-out;
}

.Page.exit    .Menu {
  animation: slideIn 600ms ease-in-out reverse;
  /*                 ๐Ÿ‘†๐Ÿ˜ฒ
   * There is no ร  priori requirement to synchronise effects:
   * Liminal detects all effects triggered by class application
   * and ensures they have all resolved before proceeding.
   */
}

@keyframes slideIn {
  from {transform: translateX(-100%)}
  to   {transform: translateX(   0%)}
}

If you wish to establish an app-wide convention of Liminal configuration, the component can be partially applied by invoking it as function with configuration input:

import {Liminal} from 'mithril-machine-tools'

const Animated = Liminal({
  base     : 'base',
  entry    : 'entry',
  exit     : 'exit',
  absent   : 'absent',
  present  : 'present',
  blocking : true,
})

m(Animated, m('.element'))

Utilities

viewOf

viewOf is used by nearly all of the MMT components. It enables a component interface that accepts a view function as input, instead of pre-compiled virtual DOM nodes. This allows you to write components which seek to expose special values to the view at call site, or control its execution context.

import {viewOf} from 'mithril-machine-tools'

function Timestamp(){
  const timestamp = new Date()
  
  return {
    view: v =>
      viewOf(v)(timestamp)
  }
}

m.mount(document.body, {
  view: () =>
    m(Timestamp, time =>
      m('p', time.toLocaleTimeString()),
    ),
}

reflow

Used when a script requires all pending DOM mutations to persist and have their effects persist to screen before proceeding, reflow returns a promise that internally queries document body dimensions to trigger reflow. Multiple reflow calls in the same tick will return the same promise, allowing queries to be batched for a minimum of DOM-thrashing. reflow is particularly useful in oncreate hooks to ensure transitions caused by temporary CSS application are not optimised away by DOM mutation batching.

import {reflow} from 'mithril-machine-tools'

m('div', {
  async oncreate({dom}){
    dom.classList.add('initial-state')
    
    await reflow() // ๐Ÿ‘ˆ without reflow, `initial-state` risks never being applied

    dom.classList.remove('initial-state')
  }
})

indexOf

Retrieves the index of the supplied nodes position within its parent nodes list of immediate child nodes.

import {indexOf} from 'mithril-machine-tools'

m.mount(document.body, {
  view: () => 
    m('.Page',
      m('h1', 'Hello'),
      
      m('p', {
        oncreate : v => {
          v.dom.textContent =
            `I'm child number ${ indexOf(v.dom) }!`
        },
      }),
    ),
}

domOf

Retrieves an array of DOM nodes contained by a virtual node.

import {domOf} from 'mithril-machine-tools'

m.mount(document.body, {
  view: () =>
    m('h1', {
      oncreate: v => {
        console.assert(
          domOf(v).length === 3
          &&
          domOf(v)[0].nodeValue === 'Hello'
        )
      },
    }, 
      'Hello', ' ', 'you',
    ),
})

getSet

getSet follow the uniform access principle of virtual DOM & applies it to Maps. This enables the use of maps as a data structure which can be queried such that access code does not need to conditionally fork for whether a value associated with any given key needs to be created, or merely retrieved โ€” which can be extremely useful in writing expressive queries that work with the grain of Mithril applications. This is used in the Static module to determine the rendering context of Live components.

import {getSet, Promiser} from 'mithril-machine-tools'

const requests = new Map

m.route(document.body, '/user/barney', {
  '/user/:userId': {
    render: ({attrs: {userId}}) =>
      m(Promiser, {
        promise: getSet(requests, '/data/user/' + userId, url => 
          m.request(url)
        ),
      }, ({pending, resolved, value : user}) =>
        m('.Profile', {
          style: {
            transition: 'opacity 1s ease-in-out',
            opacity   : pending ? 0.75 : 1,
          },
        },
          resolved && [
            m('h1', user.name),
          
            m('p', user.handle),
          ],
        ),
      ),
  },
})

Table

Table behaves like a set whose contents are identified not by equality but by comparing a set of properties, which must be supplied to the table at initialisation.

import Table from 'mithril-machine-tools'

const users = new Table(['username', 'email'])

table.add({
  username: 'Barney', 
  email: '[email protected]',
  age: 35,
})

table.add({
  username: 'Barney',
  email: '[email protected]',
  age: 42,
})

console.assert(table.size === 1)

console.assert(
  table.get({
    username: 'Barney',
    email: '[email protected]',
  })
    .age === 35
)

mithril-machine-tools's People

Contributors

barneycarroll avatar github-actions[bot] avatar porsager avatar pygy 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

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar

mithril-machine-tools's Issues

(Portal) Placeholder DOM is pointless

Placeholder DOM assumes some kind of manual manipulation is necessary to inject content on the second draw, but Island hands this off to Mithril render core โ€” placeholder can consist of a nullish vnode.

Docs!

Iโ€™ve tried to write docs for this so many times but each API is kind of its own elaborate story. I may blog about these later but to be able to draw a line under a feature-complete v1 I need to put up documentation that reasonably demonstrates usage.

Iโ€™m currently thinking of doing these for every export:

  1. Typescript definition & static prose equivalent
  2. Broad use case description in plain language, one paragraph
  3. Live use case implementation demo

Support multiple classes for Liminal

I use tailwind, so it's useful to add multiple classes like this:

m(Liminal, {
  present: "transition duration-300",
  entry: "opacity-100",
  exit: "opacity-0",
}, ...)

However this throws the standard browser error:

InvalidCharacterError: The string contains invalid characters.

on a dom.classList.add line.

Should inline support POJO's?

In the mithril docs we've decided that pojo's should only be used for stateless components, closures for stateful.

Seeing as the justification for Inline is to have some state inline. It seems we could take that as an opportunity to only support closures.

What do you think?

Transition hook for Mobile Unit

Mobile Unit could be extended with a transition hook that stretched over the period before and after DOM movement to control the transition.

A generator would be useful for yielding to the point of movement and resuming thereafter. Here follows pseudo-code example tweening the element between its initial & eventual positions:

const fixed = {position: 'fixed'}

m(Unit, {
  *transition({dom, move}){
    const initial  = dom.getBoundingClientRect()
  
    yield move 
    // dom is now in its new position
  
    const eventual = dom.getBoundingClientRect()
  
    dom.animate([
      {โ€ฆfixed, initial },
      {โ€ฆfixed, eventual},
    ], options)
  }
},
  m(Video)
)

๐Ÿค”

Alternative Waiter logic

Waiter is cumbersome because it requires isolating a single detachment point - the Waiter - to trigger descendant Services. Other compound components are not so sensitive to the exact logical position of their controller component, and positioning Waiter in the correct position becomes an irritatingly subtle task. Additionally, it is impossible to indicate that a Serviced node be subject to the detachment condition of multiple nodes:

condition1 && 
  m(Waiter, Service1 =>
    m('.Page', 
      m(Content),
      
      m(Comments),

      condition2 &&
        m(Waiter, Service2 =>
          m(Editor,
            condition3 &&
              // Service1 or Service2 will foreclose the other; 
              // Defaulting on condition3 triggers neither (should it?)
          )
        )
    )
  ) 

Service logic is thus fudged inasmuch as it shadows onbeforeremove logic for its immediate children, but only on the condition of one uniquely positioned Waiters removal.

A more straightforwardly usable removal mechanism would be to employ the logic of Mobile -> Unit such that Waiter registers Service instances on every view execution, and triggers onbeforeremove logic for all newly departed Services. Thereby:

m(Waiter, Service =>
  condition1 &&
    m('.Page', 
      m(Content),
      
      m(Comments),

      condition2 &&
        m(Editor,
          condition3 &&
            m(Service, {key: 'modal'}
              m(Modal) 
            )
        )
    )
  ) 

With the new logic, Services' key communicates the deferred removal condition, rather than the position of its Waiter controller.

Fix Island redraw function overload

The event-handler-wrapping overload inference eagerly assumes an argument, which causes the function to break when it's called directly without.

(Island) Using `m.render` on intervening DOM nodes is overly complicated and redundant

Because of nested fragments, components immediately returning components, etc, etc, Island needs to account for the fact that its nearest DOM-matched ancestor may be several vnodes away. We cater for this by creating a clone of the 'host' vnode entity and creating a hot path down to the recomputed Island view.

But seeing as we have to tunnel through any number of intervening vnodes anyway, would we be better off avoiding the intermediate host entity and simply rendering the patched structure to the original root?

npm usage?

Are we going with opponents? It'd be nice to start using proponents in real projects even though it is exploratory.

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.