m2sfwq
Last Updated: February 25, 2016
·
1.922K
· abe33

Lightweight Mixins in CoffeeScript

The Mixin class:

class Mixin
  @attachTo: (klass) ->
    klass::[k] = v for k,v of this.prototype when k isnt 'constructor'
    @included? klass

Edit: The original version did not test the property key, it resulted in the original constructor not being accessible anymore.

A simple mixin:

class Suspendable extends Mixin

  start: -> 
    # lazy context binding of handleFrameRequest
    unless @handleFrameRequest.bound
      f = @handleFrameRequest
      @handleFrameRequest = => f.apply this, argument
      @handleFrameRequest.bound = true

    # animation start
    unless @animated
      @lastTime = new Date()
      requestAnimationFrame @handleFrameRequest
      @animated = true

  stop: -> @animated = false if @animated

  handleFrameRequest: -> 
    if @animated
      currentTime = new Date()
      delta = currentTime - @lastTime
      @animate delta
      requestAnimationFrame @handleFrameRequest
      @lastTime = currentTime

  animate: -> # override to create your animations

Parameterized mixins:

Equatable = (properties...) ->
  class extends Mixin
    equals: (o) -> o? and properties.every (p) =>
      if @[p].equals? then @[p].equals o[p] else o[p] is @[p]

Formattable = (classname, properties...) ->
  class extends Mixin
    toString: ->
      if properties.length is 0
        "[#{classname}]"
      else
        formattedProperties = ("#{p}=#{@[p]}" for p in properties)
        "[#{classname}(#{formattedProperties.join ', '})]"

    classname: -> classname

Decorating a Class:

class Dummy
  Suspendable.attachTo Dummy
  Equatable('position', 'speed').attachTo Dummy
  Formattable('Dummy','position', 'speed').attachTo Dummy

  constructor: (@position, @speed) ->

  animate: (delta) ->
    @position = @position.add(@speed.scale(delta / 1000))
    # ...

Nice way to wrap many mixin inclusions:

class Rectangle
  PROPERTIES = ['x','y','width','height','rotation']

  [
    Equatable.apply(null, PROPERTIES)
    Formattable.apply(null, ['Rectangle'].concat PROPERTIES)
    Sourcable.apply(null, ['geomjs.Rectangle'].concat PROPERTIES)
    Parameterizable('rectangleFrom', {
      x: NaN
      y: NaN
      width: NaN
      height: NaN
      rotation: NaN
    })
    Cloneable,  Geometry,       Surface
    Path,       Triangulable,   Intersections
  ].forEach (mixin) -> mixin.attachTo Rectangle

#....

Edit: As to respond to @sheerun suggestion, some other way to wrap mixins inclusions:

Through a base Module class:

class Module 
  @include: (mixins...) ->
    if Object::toString.call(mixins[0]).indexOf('Array') >= 0
      mixins = mixins[0]

    mixins.forEach (mixin) -> mixin.attachTo this

class Dummy extends Module
  @include Suspendable, 
           Equatable('position', 'speed'),
           Formattable('Dummy','position', 'speed')

Or if you can't, or don't want to use inheritance:

 include = (mixins...) ->
  if Object::toString.call(mixins[0]).indexOf('Array') >= 0
    mixins = mixins[0]

  in: (klass) -> mixins.forEach (mixin) -> mixin.attachTo klass

class Dummy extends Module
  include(
    Suspendable, 
    Equatable('position', 'speed'),
    Formattable('Dummy','position', 'speed')
  ).in Dummy

3 Responses
Add your response

1725

That wrapper code begs for some helper.

over 1 year ago ·
8416

What is the usage of @included in Mixin class ? I do not see any other reference here.
I guess it is a function but where is it defined ?

over 1 year ago ·
13657

You can define an included method on your class so than the class can know it's included into another class, useful for example when your mixin will generates methods whose names aren't known when defining the mixin.

over 1 year ago ·