Open maerteijn opened 3 years ago
I see now when I import the property-directive
directly that this doesn't work
import 'slim-js/property-directive'
Could it be that I need a newer version of Node (I have 14.17)?
Edit: User error in my attempt to add some debugging statements. Original questions still applies (for now).
So two things I figured out:
.who
attribute should contain an expression, not a static value (so something like {{this.getSomething()}}
)PluginRegistry
applies the property directive and binds the property before the constructor is called.Working example:
import { Slim } from 'slim-js';
import 'slim-js/property-directive';
Slim.element(
'my-greeting',
/*html*/ `
<h1>Hello, {{this.who}}!</h1>
`,
class MyGreeting extends Slim {
constructor() {
super()
if (this.hasOwnProperty("who")) {
console.log(`inside constructor: ${this.who}`);
}
}
}
);
Slim.element(
'my-app',
/*html*/ `
<my-greeting .who="{{this.who}}"></my-greeting>
<my-greeting .who="{{this.another}}"></my-greeting>
`,
class MyApp extends Slim {
who = "I am the one and only"
another = "And I'm another one!"
}
);
I know that pre-binding properties in javascript before the constructor is called is a common pattern in some frameworks, but it still feels a bit hackish to me. Maybe you could elaborate more on this?
Hi. Are you using any babel/transpiling tools? These affect the way class properties are declared. Some tools use Object.defineProperty and thus destroy the internals.
I use webpack 5 with @babel/preset-env
, so that could indeed be the case. However, as stated above, the 'slim-js/property-directive
is called before the constructor, so that would not make a difference right?
Regarding your question.
Every class that extends Slim has a static template property. It is just a string, that should represent a decent valid HTML Markup.
During the construction phase, the template is parsed by the browser (in-memory, inside a document fragment). There is a tree-walker (native, implemented by the browser) that iterates over the constructed tree. Every node that has "{{ ... }}" is being analyzed by a dedicated function. These expressions are executed every time the change is required. How Slim knows when a change is required? Simple. Aggregating all the expression (per class instance), everything that has this.*
is now a known reactive property. Slim replaces the proeprty with a getter/setter functions that triggers the change.
For example, an expression like <element attribute="{{ this.whatever }}"></element>
will bind the attribute's value to the whatever
setter. Every other change does not affect that node.
When the class is constructed, there is a dedicated function that executed the expression as-is, bound to the class' instance. Since templates are re-used, and even properties are re-used across the same template, these functions are hoisted and memoized, to save runtime and memory.
If your tool intervenes in the native class property declarations, it may destroy the mechanics. Either avoid transpiling class properties, or just initialize those in the constructor.
The directives are optional, therefore can be consumed separately. Directives are a unique form of attributes, usually with a special (but valid) character. The property directive is called every time there is an attribute that starts with a period.
The property simply executes the content of the expression every time a detected (and relevant) change occurs, and updates the property on the target element.
In that case, when you change the parent's component another
property, the directive targets the child node's instance and replaces the who
property with the value.
<parent>
<child .who="{{this.username}}"></child>
</parent>
The parent class gets a username
setter function, that triggers a changeset.
When you change the username
property's value, that changeset includes the property directive execution that receives the new value, executes the statement (this.username
), and executes child.who = newValue
;
The property simply executes the content of the expression every time a detected (and relevant) change occurs, and updates the property on the target element.
Yes and this is a really nice feature I like. It's just how the initial value of the property is set as you say:
Either avoid transpiling class properties, or just initialize those in the constructor.
So initially, in the first load the constructor of the element is called after the properties are bound to the instance (with the property directive), so should I detect that in the constructor then?
Before anything else: Thank you for answering this (and thanks for this awesome framework too!!)
So, to rule out any webpack or transpiling, a pure browser version:
<!DOCTYPE html>
<html>
<head>
<title>Properties with Slim.js</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="">
<script type="module">
import { Slim } from 'https://unpkg.com/slim-js@5.0.10/dist/index.js';
import 'https://unpkg.com/slim-js@5.0.10/dist/directives/property.directive.js';
Slim.element(
'my-greeting',
`
<h1>Hello, {{this.who}}!</h1>
`,
class MyGreeting extends Slim {
constructor(who) {
super()
if (this.hasOwnProperty("who")) {
console.log(`who property already set: ${this.who}`);
}
}
}
);
Slim.element(
'my-app',
`
<my-greeting .who="{{this.who}}"></my-greeting>
<my-greeting .who="{{this.another}}"></my-greeting>
`,
class MyApp extends Slim {
who = "I am the one and only"
another = "And I'm another one!"
}
);
</script>
</head>
<body>
<my-app></my-app>
</body>
</html>
When I define a class property called who:
class MyGreeting extends Slim {
who = "my default value
...
}
or when I initialize it in the constructor
class MyGreeting extends Slim {
constructor() {
super()
this.who = "my default who"
...
}
}
It will overwrite the initial who property defined with<my-greeting .who="{{this.who}}"></my-greeting>
. So this will only be updated again when this.who
is updated in the parent app:
var app = document.querySelector("my-app")
app.who = "Updated who!"
It will update the component as expected.
So to get around the initial value of this who
value with the .who
property is to check for the existance of the .who
property in the constructor:
if (!this.hasOwnProperty("who")) {
this.who = "My default who"
}
I don't think this is how it is intended?
It sounds like something needs to be fixed. I'll try your example as-is.
Interestingly enough, the test in https://github.com/slimjs/slim.js/pull/112/commits/f5b5fbf5c30e85e38a7cb508683a9264cc72ec4a is passing, so in the JSDOM browser, the constructor is called before the directive is handling the property.
The constructor should always be called first.
When I create a simplified example of the example on the Creating an Element documentation page:
I expect that
this.who
will be set with the.who
property, but whatever I do, it staysundefined
.I guess I don't understand the concept of the
.who
property, could you explain how this works?