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

How does the property directive work? #111

Open
maerteijn opened this issue Nov 8, 2021 · 11 comments
Open

How does the property directive work? #111

maerteijn opened this issue Nov 8, 2021 · 11 comments
Labels

Comments

@maerteijn
Copy link
Contributor

maerteijn commented Nov 8, 2021

When I create a simplified example of the example on the Creating an Element documentation page:

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(who) {
      super()
      this.who = who;
    }
  }
);

Slim.element(
  'my-app',
  /*html*/ `
  <my-greeting .who="{{this.who}}">
  </my-greeting>
  `,
  class MyApp extends Slim {
    who = "I am the one and only"
  }
);

I expect that this.who will be set with the .who property, but whatever I do, it stays undefined.

I guess I don't understand the concept of the .who property, could you explain how this works?

@maerteijn
Copy link
Contributor Author

maerteijn commented Nov 8, 2021

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).

@maerteijn
Copy link
Contributor Author

maerteijn commented Nov 9, 2021

So two things I figured out:

  • The .who attribute should contain an expression, not a static value (so something like {{this.getSomething()}})
  • The 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?

@eavichay
Copy link
Member

eavichay commented Nov 9, 2021

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.

@maerteijn
Copy link
Contributor Author

maerteijn commented Nov 9, 2021

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?

@eavichay
Copy link
Member

eavichay commented Nov 9, 2021

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.

@eavichay
Copy link
Member

eavichay commented Nov 9, 2021

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;

@maerteijn
Copy link
Contributor Author

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?

@maerteijn
Copy link
Contributor Author

maerteijn commented Nov 9, 2021

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?

@eavichay
Copy link
Member

It sounds like something needs to be fixed. I'll try your example as-is.

@eavichay eavichay added the bug label Nov 15, 2021
@maerteijn
Copy link
Contributor Author

Interestingly enough, the test in f5b5fbf is passing, so in the JSDOM browser, the constructor is called before the directive is handling the property.

@eavichay
Copy link
Member

The constructor should always be called first.

eavichay pushed a commit that referenced this issue Sep 6, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

2 participants