Skip to content

Design doc

Justin Meyer edited this page May 21, 2019 · 50 revisions

Big Example

The following example shows most of the possibilities:

import {DefineClass, type} from "can/ecosystem";

MyExampleType extends DefineClass {
  static define = {

  }

  get property(){}
  set property(){}

  method(){}
}

Packages, modules and exports

Currently, we are going to release the following packages with associated exports and named can/ecosystem exports:

  • can-define-class exports the DefineClass constructor. import {DefineClass} from "can/ecosystem"
  • can-define-array exports the DefineArray constructor. import {DefineArray} from "can/ecosystem"
  • can-stache-define-element exports the StacheDefineElement constructor. import {StacheDefineElement} from "can/ecosystem"
  • can-type exports an object of properties and methods. import {type} from "can/ecosystem"

Issues:

  • Are these the right packages?
  • Should define-class have a single export
  • DefineArray package name
  • Does DefineArray inherit from Array? If it does, there will be bugs due check to Array.isArray assuming non observable.
    • DefineArray overwrites array methods so events will fire. This is why an Array type is needed.
    • A proxy version of DefineArray is very useful / important. However, is this incompatible with class?
  • Is there a "version locking problem" with having can-define-class and can-define-list? cc @kevin

can-type

This is an improved can-data-types. 3.0 had a can-types.

Examples:

age: types.maybe(Number), //-> {[can.new](), schema: {type:"or", values: [Number, null]}}
age: types.convert(Number), //-> {[can.new]()}
    ALTERNATES:
    types.coerce(Number)

age: types.maybeConvert(Number),
    ALTERNATES:
    age: types.loose(Number)

age: types.check(Number)
    ALTERNATES:
    age: types.strict(Number)


age: types.any 
    - not "*"

Issues:

type.maybe( type: ConstructorFunction | TypeObject ): TypeObject

This converts a type Type to allow null and undefined.

Params:

  • type - A constructor function or TypeObject

Returns:

  • A TypeObject

type.convert( type: ConstructorFunction ): TypeObject

Issues:

  • Can this be made to take a TypeObject?

type.maybeConvert( type: ConstructorFunction | TypeObject): TypeObject

Issues:

  • Can this be made to take a TypeObject?

type.check( type: ConstructorFunction | TypeObject): TypeObject

This will return a type that will throw if can.new is called with value that is not already the right type.

canReflect.new( type.check(String), 1 ) //-> throws

type.any: TypeObject

A type that allows anything to be set.

canReflect.new( type.any, 1 ) //-> 1
canReflect.new( type.any, {} ) //-> {}

DefineClass

Schema:

class Extended extends DefineClass {
  static seal = Boolean;
  
  static defaults = Primitive |
       PrimitiveFunction | 
       ConstructorFunction |
       TypeObject |
       PropDefinition;

  static define = PropDefinitions;

  get PROP_NAME(){}
  set PROP_NAME(){}

  METHOD_NAME(){}
}

new ExtendedDefineClass({ [key:String]: Any })

Create an instance of a defined class.

Issues:

new DefineClass(Object{ [key:String]: Any })

Creates an instance of a define class.

Example:

const obj = new DefineClass({ foo: "bar" })

obj.foo //-> "bar"

Issues:

seal

Controls if the object is sealed or not.

Issues:

defaults

This would describe the default property definition object. This would be used for all properties for the object. The possible values would match the key values of a PropDefinitions object.

Issues:

Other methods and properties

  • static can.defineInstanceProperty - so can-connect can add _saving
  • listenTo / stopListening
  • get(prop) and set(prop, value)

Issues:

  • get() and set() should not work with a sealed instance. But if we add a proxy, what's the point of these?
  • How will StacheDefineElement be able to do this.listenTo("click", function(){})

PropDefinitions

Schema:

class Extended extends DefineClass {
  static define = PropDefinitions {
    [key: String]:
       Primitive |
       PrimitiveFunction | 
       ConstructorFunction |
       TypeObject |
       PropDefinition |
  }
}

Example:

class Extended extends DefineClass {
  static define = {
    age: 36,                    // Primitive
    age: Number                 // PrimitiveFunction
    date: Date                  // Constructor function
    name: type.convert(String)  // TypeObject
    completed: {default: false} // PropDefinition
  }
}

Depending on the key's value in a prop definition, the following will happen:

  • Primitive - Uses the primitive as the default value. This also sets the type to match the type of the primitive value.

    Issues:

  • PrimitiveFunction - Sets the type to the PrimitiveFunction. This will be have "strict" typing. Primitives like Number will use a typeof check to verify.

  • ConstructorFunction - Sets the type to the ConstructorFunction value. This will be have "strict" typing. Non primitives will use an instanceof check.

  • TypeObject- This will be the type, set as-is.

  • PropDefinition - If an object is passed, it will be treated as a PropDefinition object.

Issues:

  • What if the PropDefinition object is empty? This was a shorthand for the any type in can-define.
  • What if a function is specified? Could this be treated as a default value too? This would be nice for views:
    static define = {
      renderer: stache("{{foo}}")
    }

PropDefinition

Schema:

{
  type: 
        TypeObject | 
        PrimitiveFunction |
        ConstructorFunction | 
        FunctionFunction      //?

  default: 
        Primitive |
        Function |
        Object

  get default(){}
  
  get(){}
  set( [newVal] [,lastSet] ){}
  async( [resolve] ){}
  value( {resolve, listenTo, stopListening, lastSet}){},

  required: Boolean=false,
  enumerable: Boolean,
  serialize(value):Any
}

Example:

{
  type: type.convert(String) // TypeObject
  type: Date                 // ConstructorFunction
  type: Number               // PrimitiveFunction
  type: Function             // FunctionFunction ??

  default: 1                 // Primitive
  default(){ return {}; }    // Function
  default: {}                // Object

  get default(){ return {}; }
  
  get(){
    return this.first + " " + this.last;
  }
  set(){
    this.sideEffect = 1;
  }
  set(value){ 
    return value;
  }
  set(value, currentValue){
    return currentValue;
  } 
  async(){
    return Todo.getList();
  }
  async(resolve){
    Todo.getList().then(resolve);
  }
  value( {resolve, listenTo, stopListening, lastSet}){
    resolve(5);
  }
  required: true,
  enumerable: false
  serialize: value => ""+value
}

type

default

Does the following based on the value:

  • Primitive - Uses that primitive value as a default value.
  • Function - Uses that primitive value as a default value.
    static define = { myRenderer: { default: stache(`look at me`) } };
    
    If you want a new function to be created everytime, use get default(){}
  • Object - Warn user to use get default(){}.

Issues:

get default

Issues:

get

set

Issues:

async

value

required

A value be passed on initialization. Undefined counts as being passed: new Extended({prop: undefined}).

Issues:

  • Warn if true when there's a default property?

enumerable

If the property should be enumerable. Non-enumerable properties are not serialized.

serialize

How to serialize the value.

Other Types

Reflect

The following are some important methods:

  • canReflect.convert()
  • canReflect.new()
  • canReflect.isConstructorLike()
  • canReflect.isMember() ... does this exist?

TypeObject

Properties:

  • can.new(value) - Throw if not allowed, otherwise converts to the type.
  • can.isMember(value) - Return true if a member of Type

ConstructorFunction

Any constructor function, including Builtins that pass canReflect.isConstructorLike()

Examples:

class Todo {
 method(){}
}
Date

How to define "hungry" properties

https://github.com/canjs/can-define-class/issues/45

HungryDefineArrayOrObject {

}

HugryList  extends DefineArray {
    define {
        "#": {
            type: type.maybeConvert([Hungry|DefineList]) // does not work
        }
    }
}

Hungry extends DefineClass {
    define {
        "*": {
            type: type.maybeConvert([Hungry|DefineList]) // does not work
        }
    }
}

Todo extends DefineClass {
    static define = {
        meta: Hungry
    }
}


var todo = new Todo();

todo.meta = {
    foo: {bar: {}}
}

todo.meta.foo.bar.zed = "ted"

Setting properties related to Type

class Type extends DefineClass {
  static define = { prop: Number }
}
const instance = new Type();
instance.prop = "1";  //-> throws `"1" is not a Number`
instance.prop = null //-> throws `null is not a Number`
Clone this wiki locally