Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

es6 class compatibility #247

Open
ianwremmel opened this issue Aug 5, 2016 · 3 comments
Open

es6 class compatibility #247

ianwremmel opened this issue Aug 5, 2016 · 3 comments

Comments

@ianwremmel
Copy link

ampersand-state has been immensely useful for backing my models in a consistent fashion, but I'm finding more and more that es6 classes would solve a category of problems that ampersand-state does not.

I'd like to do some work on making es6 classes and ampersand play nicely together, but before I begin, I'm curious what (if any) prior work has been done on this and if the maintainers have any thoughts on the best way to go about it.

A naive solution may be to simply reimplement ampersand-state as a class (possibly relying on decorators for defining prop/session/computed), but that'll force end users into a precompiler step. Can anyone share solutions that have already been considered?

@STRML
Copy link
Contributor

STRML commented Sep 7, 2016

This is an excerpt of how we're wrapping them for use with ES6 (the primary motivation was compatibility with Flow, and I couldn't get the AmpersandModel.extend() syntax to type properly):

stateModel.js

// @flow
import AmpersandState from 'ampersand-state';
import _ from 'lodash';

export default class StateModel<Props: GenericMap<mixed>> extends AmpersandState<Props, *, *> {

  // By default set to 'reject' to throw errors when unknown properties are added in development
  // 'ignore' in production to squelch errors.
  static extraProperties = 'ignore';

  // Keys helper
  keys() {
    return _.keys(this.all);
  }

  rawKeys() {
    return _.keys(this._values);
  }

  // Simple values getter
  values(): Object {
    return this.serialize();
  }

  // getType as-is is useless, let's make it useful
  getType(key: string): ?string {
    return this._definition[key] && this._definition[key].type;
  }
}

// ES6 Class Compatibility
export function setupModel<T: Class<$Subtype<StateModel<any>>>>(AModel: T): T {
  // Move ampersand-specific statics over to prototype for compatibility
  AModel.prototype.typeAttribute = AModel.typeAttribute;
  AModel.prototype.idAttribute = AModel.idAttribute;
  AModel.prototype.props = AModel.props;
  AModel.prototype.derived = AModel.derived;
  AModel.prototype.children = AModel.children;
  AModel.prototype.extraProperties = AModel.extraProperties || AModel.prototype.extraProperties;

  return AModel.extend(AModel.prototype);
}

// DataTypes

// Get existing datatypes to extend
const dataTypes = AmpersandState.extend({props: {}}).prototype._dataTypes;

AmpersandState.prototype._dataTypes = {
  ...dataTypes,
  number: {
    set(newVal) {
      // This prevents a coercion to 0 (Number(null)), allowing defaults to work properly
      if (newVal === null) return {val: null, type: null};
      return {
        val: Number(newVal),
        type: 'number'
      };
    },
    get(val) {
      return val;
    },
    default() {
      return NaN;
    }
  },
  integer: {
    set(newVal) {
      if (typeof newVal === 'number' || typeof newVal === 'string') {
        return {
          val: parseInt(newVal, 10),
          type: 'integer'
        };
      }
      return {
        val: newVal,
        type: typeof newVal
      };
    },
    get(val) {
      if (typeof val === 'number') {
        return parseInt(val, 10);
      }
      return null;
    },
    default() {
      return NaN;
    }
  }
};

interfaces/ampersand-state.js

declare module 'ampersand-state' {
  declare type AmpersandType = 'integer' | 'number' | 'string' | 'date' | 'moment' | 'object' | 'currency';
  declare type DerivedDef<Props, DerivedProps> = {
    deps?: Array<$Keys<Props> | $Keys<DerivedProps>>,
    fn: () => any
  };
  declare type ExtraPropDef = {
    type?: AmpersandType,
    default?: any
  } | string;
  declare type CreateOptions = {
    foo?: string
  };
  declare type GenericMap<T, U = any> = {[key: $Keys<U>]: T};
  declare type DataType<T> = {
    get: (val: T) => T;
    default: () => T;
    set: (newVal: any) => {val: T, type: ?string};
  };
  declare type GetAttributesOpts = {
    props?: boolean,
    session?: boolean,
    derived?: boolean
  };

  declare class AmpersandState<Props, ExtraProps: GenericMap<ExtraPropDef>, DerivedProps: GenericMap<DerivedDef<*, *>>> {
    static idAttribute?: string | false;
    static typeAttribute?: string | false;
    static extraProperties?: 'ignore' | 'reject';
    // static extend<T1>(def: T1): Class<AmpersandState<Props, DerivedProps> & T1>;
    // static extend<T1, T2>(def: T1, def2: T2): Class<AmpersandState<Props, DerivedProps> & T1 & T2>;
    // static extend<T1, T2, T3>(def: T1, def2: T2, def3: T3): Class<AmpersandState<Props, DerivedProps> & T1 & T2 & T3>;
    // static extend<T1, T2, T3, T4>(def: T1, def2: T2, def3: T3, def4: T4): Class<AmpersandState<Props, DerivedProps> & T1 & T2 & T3 & T4>;

    static derived: ?DerivedProps;
    static props: ?ExtraProps;
    static extend(...protos: Array<Object>): typeof AmpersandState;
    // Added statics
    static schemaProps: Props;
    static allProps: Props & ExtraProps & DerivedProps;

    // Make these have an indexable signature
    [key: $Keys<Props> | $Keys<DerivedProps>]: *;

    collection: ?Object; // NOTE: If you try to import Collection here, it will silently coerce it to any 
    indexes: ?Array<string>;
    isCollection: (item: any) => boolean;
    length: number;
    mainIndex: string;
    isModel: (item: any) => boolean;

    // Methods
    initialize(attributes: ?Object, options: ?Object): void;
    getType(key: string): ?string;
    get(key: string): any;
    getAttributes(options: GetAttributesOpts, raw?: boolean): GenericMap<mixed, Props>; // JSON
    serialize(): GenericMap<mixed, Props>; // JSON
    toJSON(): GenericMap<mixed, Props>; // JSON
    on(event: string, handler: Function): void;
    off(event: ?string, handler: ?Function): void;

    // Added methods
    keys(): Array<$Keys<Props>>;
    rawKeys(): Array<$Keys<Props>>;
    values(): GenericMap<mixed, Props>;

    // Internals
    all: {[key: $Keys<Props> | $Keys<DerivedProps>]: mixed};
    // TODO internal definition
    _definition: {[key: $Keys<Props> | $Keys<DerivedProps>]: Object};
    _dataTypes: GenericMap<DataType<any>>;
    _derived: {[key: $Keys<DerivedProps>]: DerivedDef<Props, DerivedProps>};
    _values: Props;
  }
  declare var exports: typeof AmpersandState;
}

We then have a number of helpful Flow type getters:

export type ModelSchemaProps<T: Model> = $PropertyType<Class<T>, 'schemaProps'>; // extracts Props from Model
export type ModelShape<T: Model> = $Shape<T & ModelSchemaProps<T>>;
export type ModelKeys<T: Model> = $Keys<T> | $Keys<ModelSchemaProps<T>>;

@ianwremmel
Copy link
Author

Yea, my motivation is similar. Lots of tooling seems work quite well with the new class syntax, but doesn't infer anything from AmpersandState.extend. I started this pr a few weeks ago. My strategy was more a long the lines of reimplement everything, but (a) borrow heavily from ampersand-state.js, (b) replace derived/props/session with decorators (c) make sure the original test suite passes (thus, requiring extend to work for backwards compatibly. It's still very much a work in progress, but other than some details around constructor/initialize order of operations, I think it'll work.

@ianwremmel
Copy link
Author

(Note: that PR i linked is huge because it includes a copy of the ampersand-state test suite - if I ever get it to where I think it's actually usable, I planned on seeing about getting it into ampersand-state proper)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants