Skip to content

Instantly share code, notes, and snippets.

@a-s-o
Last active August 29, 2015 14:27
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save a-s-o/1ecdb5584a099ec9cc3c to your computer and use it in GitHub Desktop.
Save a-s-o/1ecdb5584a099ec9cc3c to your computer and use it in GitHub Desktop.
Lifecycle hooks for mithril components
m.createComponent = function createClass (displayName, opts) {
const hooks = _.defaults(opts, hookDefaults); // Apply some defaults (excluded)
const methods = _.methods(_.omit(opts, hookNames)); // Get instance methods
const propTypes = createValidator(hooks.propTypes, displayName); // Create a prop type validator (excluded)
const Component = {
controller () {
const initialState = hooks.getInitialState() || {};
const ctrl = {
defaultProps: hooks.getDefaultProps() || {},
state: initialState,
prevState: initialState,
setState (obj) {
ctrl.prevState = ctrl.state;
ctrl.state = _.extend({}, ctrl.state, obj);
},
render (nextProps, ...args) {
if (ctrl.isMounted !== true) {
// Set initial props so they can be copied
// over to prevProps on first run
ctrl.props = nextProps;
// Create the config fn
ctrl.config = makeConfig(ctrl, hooks);
} else {
hooks.componentWillUpdate(ctrl, nextProps);
}
// Save a copy of previous props
ctrl.prevProps = ctrl.props;
ctrl.props = nextProps;
// Call the component's render method and get the view output
const output = hooks.render.apply(wrapper, [ctrl, ...args]);
// If top level element then wrap the config
if (_.has(output, 'attrs')) {
wrapConfig(output.attrs, ctrl.config);
return output;
}
// Wrap all other content
// ex: when output = m.component(SomeComponent)
// FIX: after https://github.com/lhorie/mithril.js/issues/761
return m('span', { config: ctrl.config }, output);
}
};
function wrapper (fn) {
return function wrapped () {
return fn.apply(wrapper, [ctrl, ...arguments]);
};
}
_.each(methods, function addMethods (methodName) {
ctrl[methodName] = wrapper(opts[methodName]);
});
hooks.componentWillMount(ctrl);
return ctrl;
},
view (ctrl, params, children) {
let nextProps;
if (__DEV__) {
nextProps = validateProps(displayName, propTypes, params, children);
} else {
nextProps = params;
nextProps.children = children;
}
_.defaults(nextProps, ctrl.defaultProps);
if (hooks.shouldComponentUpdate(ctrl, nextProps) === false) {
return { subtree: 'retain' };
}
const args = [];
const len = arguments.length;
for (let i = 2; i < len; i++) args[i - 2] = arguments[i];
return ctrl.render(nextProps, ...args);
}
};
return _.extend(Component, hooks.statics);
};
// Wraps a config function
function wrapConfig (attrs, config) {
const original = attrs.config;
if (!original) {
attrs.config = config;
} else {
attrs.config = (a, b, c, d) => {
if (original) original(a, b, c, d);
config(a, b, c, d);
};
}
}
// Makes a config function which calls dom lifecycle hooks
function makeConfig (ctrl, hooks) {
return function config (element, isInitialized, ctx, vdom) {
if (!isInitialized) {
hooks.componentDidMount(ctrl, element, vdom);
ctrl.isMounted = true;
ctx.onunload = _.wrap(ctx.onunload, (originalUnload) => {
if (originalUnload) originalUnload();
hooks.componentWillUnmount(ctrl, element, vdom);
});
} else {
hooks.componentDidUpdate(ctrl, ctrl.prevProps, ctrl.prevState);
}
};
}
@a-s-o
Copy link
Author

a-s-o commented Aug 13, 2015

Usage:

   const ListEditor = m.createComponent({
      propTypes: {
         list : list(Obj),
         type : enums.of('bulleted numbered checklist'),
         onmove   : Func,   // [oldIndex:Num, newIndex:Num]-> Nil
         onupdate : Func    // [index:Num, item:Obj]-> Nil
      },

      componentDidMount (self, element) {
         self.sortable = new sortable(element, {
            animation: 200,
            handle: '.ListEditorItem-handle',
            draggable: '.ListEditorItem',
            ghostClass: 'ListEditorItem--ghost',
            onUpdate: function moveItem ({ oldIndex, newIndex }) {
               self.props.onmove(oldIndex, newIndex);
            }
         });
      },

      componentWillUnmount (self) {
         self.sortable.destroy();
      },

      render (self) {
         return m('ol.ListEditor', _.map(self.props.list, self.renderItem));
      },

      renderItem (self, item, idx) {
         return m('li.ListEditorItem', { key: item.key }, [
            m('i.ListEditorItem-handle icon-dot-3'),
            m.component(Textarea, {
               rows: 1,
               value: item.text,
               onchange: m.withAttr('value', (update) => {
                  self.props.onupdate(idx, _.defaults({ text: update }, item));
               })
            })
         ]);
      }

   });

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment