Skip to content

Morphic

linusha edited this page Apr 26, 2023 · 7 revisions

Building stuff with morphs

Defining custom behaviour

Just create a subclass for Morph or any other existing Morph class and define custom behavior for the particular purpose.

Properties

To serialize properties of a morph, use the properties. These are declared in the method:

static get properties () {
return {
   caption: {
      defaultValue: 'Default caption',
      ...
   },
   ...
}}

You can declare a lot of things in a property:

descriptor: {
//   get: FUNCTION          - can use `getProperty('propertyname') to retrieve the value of the actual property for manipulation
//   set: FUNCTION          - should use `setProperty('propertyname', val)` to actually store the value inside of the property
//   defaultValue: OBJECT   - optional, pay attention to caveats below
//   initialize: FUNCTION   - optional, function that when present should
//                            produce a value for the property. Run after object creation.
//                            In case a property value is already present at the point of
//                            initialisation (either the defaultValue or a value passed to the constructor)
//                            it is passed the the initialize() function as an argument.
//                            Does not use custom getters and setters.
//   autoSetter: BOOL       - optional, true if not specified
//   usePropertyStore: BOOL - optional, true if not specified.
//   priority: NUMBER       - optional, true if not specified.
//   before: [STRING]       - optional, list of property names that depend on
//                            the descriptor's property and that should be
//                            initialized / sorted / ... *after*
//                            it. Think of it as a constraint: "this property
//                            needs to run before that property"
//   after: [STRING]        - optional, list of property names that this property depends on
//   internal: BOOL         - optional, if specified marks property as meant for
//                            internal housekeeping. At this point this is only used
//                            documentation and debugging purposes, it won't affect
//                            how the property works
//   readOnly: BOOL         - optional, if set prevents that the property is mutated. Also
//                            prevents the serializer from storing that value state.
//   derived: BOOL          - optional, Prevents serialization if true
}

Property inheritance

Properties of superclasses get inherited, but any property definition in a class overwrites the property definition in the superclass.
Recommendation: Only add new properties in a class, don't overwrite properties. If you need to add functionality to a property in a subclass, connect that property with a method. Take layout as example:

class Container extends Morph {
   initialize () {
      connect(this, 'extent', this, 'relayout');   // relayouts container whenever the extent changes
   }
   relayout () {
      // adjust layout of submorphs ...
   }
}

Setting multiple properties

You can set multiple properties at once using Object.assign. This is especially useful after loading and initializing a master component.

Object.assign(target, {
   name: "name",
   draggable: false,
   ...
});

Caveats

Shared default values

Setting a default value in a property can lead to multiple objects always referencing the same object as their property. See this example:

class ExampleMorph extends Morph {
   static get properties () {
      return {
         ui: {
            defaultValue: {}
         }
      }
   }
}

const a = ExampleMorph();
const b = ExampleMorph();

a.ui.button = new Button("Test");
console.log(b.ui.button);  // outputs '<Button "Test">'

The objects a and b are both ExampleMorphs and thus their ui attributes have the same defaultValue. Since that is the empty object {} and objects are copied by reference, a.ui and b.ui reference the same object {}. Any changes of a on a.ui happen to b.ui as well and vice versa.
To solve this issue, we always need to create a new ui object. This can be done by using initialize():

class ExampleMorph extends Morph {
   static get properties () {
      return {
         ui: {
            initialize () {
               this.ui = {};
            }
         }
      }
   }
}

Initialization and deserialization

When using the initialize keyword, the initialize behavior will be executed anew. This happens before any other values are set and does not respect the after keyword. That is why one has to make sure that initialize functions do not require additional state that may not be supplied at runtime.