fur
@jongold/further
npm i @jongold/further
fur

@jongold/further

πŸ¦„πŸŒˆπŸ„ algebraic style composition for functional UIs

by Jem Gold

1.0.3 (see all)License:MITTypeScript:Not Found
npm i @jongold/further
Readme

Further

npm CircleCI FantasyLand

Further adventures down the functional styling rabbit hole leading to the fantasy land

  • πŸ¦„ algebraic style composition
  • 🌈 compose evolutions & transformations
  • πŸ„ abstract interaction with props

Usage

import Style from '@jongold/further';
import { add, always, compose as c, evolve } from 'ramda';
import chroma from 'chroma';
import { Touchable, View } from 'react-primitives'; // or react-native etc

// define some abstract style transformations
// bumpFontSize :: CSS -> CSS
const bumpFontSize = evolve({
  fontSize: add(4),
});

// darkenText :: CSS -> CSS
const darkenText = evolve({
  color: c => chroma(a).darken(),
});

// brandify :: CSS -> CSS
const brandify = evolve({
  fontFamily: always('Circular Air Pro'),
});

// and some primitive styles
const boxShadow = {
  boxShadow: '0 2px 3px rgba(0,0,0,.25)',
};

// encapsulate a style that varies on props
const GenericButtonStyle = Style(props => ({
  fontSize: 16,
  fontWeight: 'bold',
  fontFamily: 'SF UI Display'
  backgroundColor: props.primary ? 'green' : 'blue',
  color: 'white',
}));

// and maybe another transform
// that relies on more props
const outlineify = style => Style(props => ({
  ...style,
  border: props.outline ? '1px solid currentColor' : 'none',
  color: props.outline ? style.backgroundColor : style.color,
  backgroundColor: props.outline ? 'transparent' : style.backgroundColor,
}));

// compose some of those transformations
const MyButtonStyle = GenericButtonStyle.map(
  c(bumpFontSize, darkenText, brandify)
).concat(boxShadow).chain(outlineify);

// associative, so could be written as
const MyButtonStyle = GenericButtonStyle
  .map(bumpFontSize)
  .map(darkenText)
  .map(brandify)
  .concat(boxShadow)
  .chain(outlineify);

// Notice that we have passed in any props yet
// so neither GenericButtonStyle nor MyButton
// are complete.

// Let's use it in context. I'm using Flow +
// react-primitives but neither are necessary
type P = {
  children: string,
  primary: bool,
  outline: bool,
  onPress: () => void,
}
const MyButton = (props: P) =>
  <Touchable onPress={props.onPress}>
    <View style={MyButtonStyle.resolve(props)}>
      { props.children }
    </View>
  </Touchable>

<MyButton primary={true} />
// => rendered View has style:
// {
//   fontSize: 20,
//   fontWeight: 'bold',
//   fontFamily: 'Circular Air Pro',
//   backgroundColor: 'darkGreen',
//   color: 'white',
//   boxShadow: '0 2px 3px rgba(0,0,0,.25)',
// }

<MyButton primary={false} outline={true} />
// => rendered View has style:
// {
//   fontSize: 20,
//   fontWeight: 'bold',
//   fontFamily: 'Circular Air Pro',
//   color: 'darkBlue',
//   backgroundColor: 'transparent',
//   border: '1px solid darkBlue',
//   boxShadow: '0 2px 3px rgba(0,0,0,.25)',
// }

Interoperability

Fantasy Land

Further implements FantasyLand 1, FantasyLand 2, FantasyLand 3 compatible Semigroup, Monoid, Functor, Apply, Applicative, Chain, ChainRec and Monad.

Table of contents

Documentation

Type signatures

Hindley-Milner type signatures are used to document functions. Signatures starting with a . refer to "static" functions, whereas signatures starting with a # refer to functions on the prototype.

A list of types used within the signatures:

  • Style - Instances of Style provided by St
  • Props - any JS prop object
  • CSS - raw CSS style objects

Creating Styles

Style

Style :: => (Props -> CSS) -> Style CSS
Style(props => ({
  backgroundColor: props.color,
  fontSize: 16,
});
// Style({ backgroundColor: __color__, fontSize: 16 })

of

.of :: a -> Style a

applicative

Style.of({
  backgroundColor: 'red',
  fontSize: 16,
});
// Style({ backgroundColor: 'red', fontSize: 16, });

Transforming Styles

concat

#concat :: Style a ~> Style a ~> Style a

semigroup

Style.of({ fontWeight: 'bold', fontSize: 14 }).concat({ fontSize: 16, backgroundColor: 'red' })
// Style({ fontWeight: 'bold', fontSize: 16, backgroundColor: 'red' }))

empty

.empty :: () -> Style _

monoid

Style.empty()
// Style({})

map

#map :: Style a ~> (a -> b) -> Style b

functor

Style.of({
  backgroundColor: 'red',
  fontSize: 14
}).map(style => ({
  ...style,
  fontSize: 16
});
// Style({ backgroundColor: 'red, fontSize: 16 }))

Style.of({
  backgroundColor: 'red',
  fontSize: 14
}).map(evolve({
  fontSize: x => x * 2
});
// { backgroundColor: 'red, fontSize: 24 })

chain

#chain :: Style a ~> Style a -> Style a

chain

wip

ap

#ap :: Style (a -> b) ~> Style a -> Style b

apply

Style.of(style => ({
  ...style,
  fontSize: style.fontSize * 2,
}).ap({ color: 'red', fontSize: 14 })
//

Consuming styles

resolve

#resolve :: Style a ~> Props -> CSS
const st = Style(props => ({
  backgroundColor: props.primary ? 'green' : 'gray',
  fontSize: 16,
})).map(evolve({ fontSize: add(2) }))

st.resolve({ title: 'sign up', primary: true })
// { backgroundColor: 'red', fontSize: 18 }

e.g., in a render function

const Button = props =>
  <button style={st.resolve(props)}>
    { props.children }
  </button>
// <button style="background-color: 'red', font-size: 18">sign up</button>

Contributors

Made with love and monads by (emoji key):


Jon Gold

πŸ“– πŸ’‘ πŸ‘€

James Baxley

πŸ’»

Jake Dawkins

πŸ’»

Michael Hurley

πŸ‘€