Skip to content

Instantly share code, notes, and snippets.

@code0100fun
Last active May 1, 2017 19:12
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 code0100fun/57d89f9c977aad728c000a873e7e83f2 to your computer and use it in GitHub Desktop.
Save code0100fun/57d89f9c977aad728c000a873e7e83f2 to your computer and use it in GitHub Desktop.
Ember.js Properties
import Ember from 'ember';
const { computed } = Ember;
function value(x) {
if (typeof x === 'undefined') {
return false;
} else if (x === null) {
return false;
} else {
return true;
}
}
const MyComponent = Ember.Component.extend({
initProp: null,
extProp: 1,
init() {
this._super(...arguments);
this.set('initProp', 0);
},
initPropThis: computed(function() {
return value(this.initProp);
}),
initPropGet: computed(function() {
return value(this.get('initProp'));
}),
initPropThisProto: computed(function() {
return value(this.constructor.proto().initProp);
}),
initPropCtorProto: computed(function() {
return value(MyComponent.proto().initProp);
}),
initPropThisCtor: computed(function() {
return value(this.constructor.initProp);
}),
initPropCtor: computed(function() {
return value(MyComponent.initProp);
}),
extPropThis: computed(function() {
return value(this.extProp);
}),
extPropGet: computed(function() {
return value(this.get('extProp'));
}),
extPropThisProto: computed(function() {
return value(this.constructor.proto().extProp);
}),
extPropCtorProto: computed(function() {
return value(MyComponent.proto().extProp);
}),
extPropThisCtor: computed(function() {
return value(this.constructor.extProp);
}),
extPropCtor: computed(function() {
return value(MyComponent.extProp);
}),
ctorPropThis: computed(function() {
return value(this.ctorProp);
}),
ctorPropGet: computed(function() {
return value(this.get('ctorProp'));
}),
ctorPropThisProto: computed(function() {
return value(this.constructor.proto().ctorProp);
}),
ctorPropCtorProto: computed(function() {
return value(MyComponent.proto().ctorProp);
}),
ctorPropThisCtor: computed(function() {
return value(this.constructor.ctorProp);
}),
ctorPropCtor: computed(function() {
return value(MyComponent.ctorProp);
}),
classPropThis: computed(function() {
return value(this.classProp);
}),
classPropGet: computed(function() {
return value(this.get('classProp'));
}),
classPropThisProto: computed(function() {
return value(this.constructor.proto().classProp);
}),
classPropCtorProto: computed(function() {
return value(MyComponent.proto().classProp);
}),
classPropThisCtor: computed(function() {
return value(this.constructor.classProp);
}),
classPropCtor: computed(function() {
return value(MyComponent.classProp);
}),
propNames: ['initProp', 'extProp', 'classProp', 'ctorProp'],
accessTypes: ['This', 'Get', 'ThisProto', 'CtorProto', 'ThisCtor', 'Ctor'],
});
MyComponent.ctorProp = 2;
MyComponent.reopenClass({
classProp: 3
});
export default MyComponent;
import Ember from 'ember';
export default Ember.Controller.extend({
appName: 'Ember Twiddle'
});
body {
padding: 10px;
}
.success {
color: green;
}
.error {
color: red;
}
table th {
text-align: center;
}
table td {
width: 50px;
text-align: center;
}
.hljs-string {
color: #333 !important;
background: #f8f8f8 !important;
}
pre {
background: inherit !important;
padding: 0 !important;
}
/*
github.com style (c) Vasily Polovnyov <vast@whiteants.net>
*/
.hljs {
display: block !important;
overflow-x: auto !important;
padding: 0.5em !important;
color: #333 !important;
background: #f8f8f8 !important;
}
.hljs-comment,
.hljs-quote {
color: #998 !important;
font-style: italic !important;
}
.hljs-keyword,
.hljs-selector-tag,
.hljs-subst {
color: #333 !important;
font-weight: bold !important;
}
.hljs-number,
.hljs-literal,
.hljs-variable,
.hljs-template-variable,
.hljs-tag .hljs-attr {
color: #008080 !important;
}
.hljs-string,
.hljs-doctag {
color: #d14 !important;
}
.hljs-title,
.hljs-section,
.hljs-selector-id {
color: #900 !important;
font-weight: bold !important;
}
.hljs-subst {
font-weight: normal !important;
}
.hljs-type,
.hljs-class .hljs-title {
color: #458 !important;
font-weight: bold !important;
}
.hljs-tag,
.hljs-name,
.hljs-attribute {
color: #000080 !important;
font-weight: normal !important;
}
.hljs-regexp,
.hljs-link {
color: #009926 !important;
}
.hljs-symbol,
.hljs-bullet {
color: #990073 !important;
}
.hljs-built_in,
.hljs-builtin-name {
color: #0086b3 !important;
}
.hljs-meta {
color: #999 !important;
font-weight: bold !important;
}
.hljs-deletion {
background: #fdd !important;
}
.hljs-addition {
background: #dfd !important;
}
.hljs-emphasis {
font-style: italic !important;
}
.hljs-strong {
font-weight: bold !important;
}
{{format-markdown "
# Ember.js Properties
**TLDR;** Never use `MyObject.myProp = aValue`!
When choosing where to attach a property on an `Ember.Object`, think about your intended use for the property.
## Instance Properties
Probably the most common use case is accessing the property from a derived instance of the prototype with the `get` function:
```javascript
const MyObject = Ember.Object.extend({
extProp: 1,
extPropGet: Ember.computed(function() {
return this.get('extProp');
})
});
```
You may have noticed the above `extProp` lives on the prototype of `MyObject`. Since the value is a number it has no side ill side-efects if you call `this.set('extProp', 2)` you will change only the one instance. If instead you had `extProp: [1]` then the array would be copied to the `proto` of all instances. Then if you called `this.get('extProp').push(1)`, you would change the contents for **all** instances of `MyObject`! If that's not the desired behavior you should assign reference values during `init`:
```javascript
const MyObject = Ember.Object.extend({
initProp: null,
init() {
this._super(...arguments);
this.set('initProp', [0]);
},
initPropGet: Ember.computed(function() {
return this.get('initProp');
})
});
```
The `initProp: null` is not used but it serves the purpose of alerting developers that instances will assign a value to it later. Look in the table below and you'll see that is is still `null` in the `proto` column (I'll get to that in a bit).
## \"Class\" Properties
Another possible use case is accessing properties directly off the constructor. In vanilla JS that would look like:
```javascript
function MyObject() {
this.constructor.ctorProp; // 2
}
MyObject.ctorProp = 2;
```
Having properties on the constructor is sometimes useful when you want to attach functions to the constructor to implement something akin to a class method.
```javascript
function Person(attrs) {
this.name = attrs.name;
}
Person.http = someHttpClient;
Person.find = function(id) {
let attrs = this.http.get('/people/' + id);
return new Person(attrs);
};
let p = Person.find(1);
console.log(p.name); // Bob
```
If you implement a similar pattern using `Ember.Object.extend` you will notice this no longer works:
```javascript
const MyObject = Ember.Object.extend({
ctorPropThisCtor: Ember.computed(function() {
return this.constructor.ctorProp; // null!
})
});
MyObject.ctorProp = 2;
```
As long as you have a reference to the actual constructor you can work around it though:
```javascript
const MyObject = Ember.Object.extend({
ctorPropCtor: Ember.computed(function() {
return MyObject.ctorProp; // 2
})
});
MyObject.ctorProp = 2;
```
But there is a better way in Ember! Use `reopenClass` to set properties on the prototype:
```javascript
const MyObject = Ember.Object.extend({
classPropThisCtor: Ember.computed(function() {
return this.constructor.classProp; // 3
}),
classPropCtor: Ember.computed(function() {
return MyObject.classProp; // 3
}),
});
MyObject.reopenClass({
classProp: 3
});
```
This has the benifit of working both from the `Constructor` and from `this.constructor`.
So if someone reopened the class you can access the property with a reference to the concrete class (`MyObject`). But now you know that even the props added in `extend` are on the prototype. So, is there a way to get at those ones?
Yes! Use `MyObject.proto()`.
```javascript
const MyObject = Ember.Object.extend({
extProp: 1,
extPropThisProto: Ember.computed(function() {
return this.constructor.proto().extProp; // 1
})
});
MyObject.proto().extProp; // 1
```
You'll notice that you can access `props` through `this.constructor` or from the concrete constructor `MyObject`.
## The point
So there are a few lessons to take away from this.
1. Don't use reference types as properties in `extend`. Instead, initialize them in the `init` function.
2. Don't over-use reopen class. For value types (string, number, bool) you are safe keeping them in the `extend` block.
3. Don't use `MyObject.x = ...`. Ember does not copy it to `constructor` and it's not available through `proto` so it's inconsistent.
Below is a matrix of the cases I went over.
"}}
<table class="table table-bordered">
<tbody>
<tr>
<td></td>
<th>this.x</th>
<th>this.get('x')</th>
<th>this.constructor.proto().x</th>
<th>A.proto().x</th>
<th>this.constructor.x</th>
<th>A.x</th>
</tr>
{{#each propNames as |propName|}}
<tr>
<th>{{propName}}</th>
{{#each accessTypes as |accessType|}}
{{#with (hash
value=(get this (concat propName accessType))
) as |cell|}}
<td class="{{if cell.value 'success' 'error'}}">
{{#if (and (eq propName 'extProp')
(or (eq accessType 'This')
(eq accessType 'Get')))
}}
{{if cell.value '✔︎ *' '✖'}}
{{else}}
{{if cell.value '✔︎' '✖'}}
{{/if}}
</td>
{{/with}}
{{/each}}
</tr>
{{/each}}
</tbody>
<tfoot>
<tr>
<td colspan=7>
{{format-markdown "\* `extProp` is shared across instances"}}
</td>
</tr>
</tfoot>
</table>
{{format-markdown "
# In Practice
Pulling this all together that means you can use `reopenClass` to add \"class\" level functions but keep your class configuration in the `extend` block with the rest of your data.
```javascript
const Person = Ember.Object.extend({
url: '/people/',
http: someHttpClient,
id: null,
name: null,
reload() {
let attrs = this.constructor.find(this.get('id'));
this.setProperties(attrs);
}
});
Person.reopenClass({
find(id) {
let attrs = this.props().http.get(this.props().url + id);
return Person.create(attrs);
}
});
let p = Person.find(1);
p.reload();
```
You'll notice I used `reopenClass` there instead of `Person.find = ...`. If you'll remember that's because you lose the ability to use `this.constructor.find` if you use the latter form.
"}}
{
"version": "0.12.1",
"EmberENV": {
"FEATURES": {}
},
"options": {
"use_pods": false,
"enable-testing": false
},
"dependencies": {
"jquery": "https://cdnjs.cloudflare.com/ajax/libs/jquery/1.11.3/jquery.js",
"ember": "2.12.0",
"ember-template-compiler": "2.12.0",
"ember-testing": "2.12.0"
},
"addons": {
"ember-data": "2.12.1",
"ember-bootstrap": "0.11.3",
"markdown-code-highlighting": "0.1.0",
"ember-truth-helpers": "1.3.0"
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment