microsoft / fast

The adaptive interface system for modern web experiences.
https://www.fast.design
Other
9.28k stars 595 forks source link

fix: ESNext TypeScript compilation option breaks @attr decorator behavior #5017

Open rajsite opened 3 years ago

rajsite commented 3 years ago

πŸ› Bug Report

When using a TypeScript configuration of "target": "ESNext" creating elements with fast-foundation and fast-element results in attributes not responding to value updates.

For example, for a button modeled after the fast-button implementation:

class Button extends FoundationButton {

    @attr
    public appearance: ButtonAppearance;

    public connectedCallback(): void {
        super.connectedCallback();
        if (!this.appearance) {
            this.appearance = ButtonAppearance.Outline;
        }
    }
}

If the button is initialized without the appearance attribute set in html, <my-button></my-button>, then we expect the connectedCallback to run and update the appearance property and attribute. Checking buttonInstance.appearance returns the value "outline" as expected but the attribute did not update and the expected style did not render. Checking the adoptedStylesheets shows that the additional stylesheet is not attached indicating the behavior did not run.

Investigating shows that the appearance property is set as an owned values, ie buttonInstance.hasOwnProperty('appearance') === true. That value is shadowing the accessors for the appearance field defined by the decorator.

Stepping through the code it looks like setting ESNext enables native class fields. The initializer for the class field in the above example runs and sets the appearance property with the value undefined during construction (didn't nail down the exact timing). Then future property updates manipulate the owned value instead of triggering the accessors on the prototype chain. Stepping into the appearance assignment in the connectedCallback shows that the accessors are not triggered.

πŸ’» Repro or Code Sample

See expected behavior and current behavior.

πŸ€” Expected Behavior

When using ESNext, and specifically browser native class fields, I'd expect the same behavior as using "target": "ES2020" in my TypeScript configuration. The following shows a stackblitz webcontainer configured for ES2020 and when running shows a button with red text illustrating the appearance correctly propagated. It also shows the hasOwnProperty flag as returning false: https://stackblitz.com/edit/fast-es2020-bug-button?file=src%2Fmain.ts

image

😯 Current Behavior

The following stackblitz webcontainer is configured for ESNext and when running shows a button with black text as the appearance property does not receive the update. It also shows the hasOwnProperty flag as returning true: https://stackblitz.com/edit/fast-esnext-bug-button?file=src%2Fmain.ts

image

πŸ’ Possible Solution

Configure the TypeScript compiler for a target that does not allow ES2022 native browser class fields. For example, in tsconfig.json set "target": "ES2020".

πŸ”¦ Context

I would like to leverage native browser features where possible. I may file a separate issue but it would be nice if the other fast builds compiled to a newer TypeScript target than ES6 and relied on consumers who felt like it to use babel, etc. to target older browsers.

Or maybe include both es6 and esnext / es2021 builds in packages so native browser async / await can be used, etc. Benefits are smaller bundle sizes (generator and generator polyfills aren't injected for async / await and other features that are native).

Edit: Looks like the interactions of class fields and accessors is unintuitive at first glance for lots of folks. They newer decorators proposals may have what's needed for attribute interactions.

🌍 Your Environment

astegmaier commented 3 years ago

I hit this issue too, and spent a while digging into it before I found this bug report. I thought it would be helpful to share what I found for whoever goes to fix this in the future.

  1. You will get the same breaking behavior if you enable the useDefineForClassFields compiler option with any target. This option was introduced in typescript 3.7 to help people gradually move to a the newly-standardized class-semantics from legacy typescript behavior. The release notes have a great explanation about the history.
  2. The root cause of the issue is that the @observable and @attr decorators use Reflect.defineProperty to create accessors on the prototype of a class that uses them see code. When the properties are initialized, different things happen depending on how the code is transpiled.

    original typescript:

      class MyClass {
           @observable myProp = "something"
      }

    a. tsc default output with target other than ES2022 or ESNext:

      class MyClass {
           constructor() {
               this.myProp = "something"
           }
      }

    b. tsc behavior with useDefineForClassFields = true: (note, this similar to how babel and swc work, too)

      class MyClass {
           constructor() {
               Object.defineProperty(this, "myProp", {
                  enumerable: true,
                  configurable: true,
                  writable: true,
                  value: "something"
              });
           }
      }

    c. tsc output with target ES2022 or ESNext:

      class MyClass {
           myProp = "something"
      }

    In cases (b) and (c), the property assignment effectively blocks the getter and setter that were created on the prototype, so future changes to myProp never get seen by the observable system.

  3. Because babel and swc output is similar to (b), this is the root cause of why it's impossible to use the @attr and @observable decorators in projects that use those transpilers (see #4503 and this issue I filed with swc).
stale[bot] commented 2 years ago

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

SamuelQZQ commented 1 year ago

This is a really significant bug! Please take action!