refi64 / vuedart

Create Vue web apps in Dart
https://refi64.com/vuedart
310 stars 19 forks source link

New functional API #42

Open refi64 opened 5 years ago

refi64 commented 5 years ago

https://github.com/vuejs/rfcs/pull/42 is the new proposed preferred API for Vue 3, and I've been really trying to think hard about how to best apply this to the more traditional OO, nominally typed Dart.

Here's basically the API I have in mind:

@vue
class MyComponentState extends State {
  // Note that there's no explicit Value<...> or Computed wrappers here,
  // because I couldn't think of any way to do that without feeling ugly.
  int count = 0;
  int get plusOne => count + 1;
  void increment() => count++;

  MyComponentState() {
    watch(() => count * 2, (val) => print('count * 2 is $val'));
    onMounted.listen(() => ...);
  }
}

@vue
class MyComponent extends Vue<MyComponentState> {
  final template = '''
    ...
  ''';

  final String prop1;
  final int prop2;

  void setup() => MyComponentState();
}

Merging state

One problem that persist is how we should handle merged state, e.g. the Dart version of this in setup:

const Component = {
  setup() {
    const { x, y } = useMouse()
    const { z } = useOtherLogic()
    return { x, y, z }
  }
}

Most likely this will continue to use Dart's mixins for the best typing support:

class ComponentState extends State with MouseLogic, OtherLogic {
  // MouseLogic, OtherLogic's own constructors initialize their stuff.
  // ...
}

class Component extends Vue<ComponentState> {
  void setup() => ComponentState();
}

However, this does re-introduce the problem of namespace conflicts, which I quite frankly am not sure how to solve yet.

The more basic reactivity API will still be supported (e.g. var v = value(0)), and all State will probably be is some sort of base class with a createVueState method that returns the state in the format Vue wants.

Timeline: I have no idea, this is likely going to be paired with some fundamental backend changes in order to utilize Dart's part system to avoid screwing with line numbers, so it's going to take a bit. Definitely messes with the 0.5/0.6 release plans I was shooting for.

nandin-borjigin commented 5 years ago

Just sharing a thought (probably infeasible)

How about a heavy use of code generation packages like 'build', 'source_gen' and 'analyzer' ?

Code that library user needs to write, say component.dart

class MouseLogic {
  int x = 0;
  int y = 0;
  MouseLogic() {
    // some magic
  }
}
class Component {
  void setup() {
    final mousePos = MouseLogic();
    final other = OtherLogic();
    return {
      x: mousePos.x,
      y: mousePos.y,
      z: other
    };
}

Code that generated by the library with the code above as input, say component.vue.dart.

class _State {
  int x;
  int y;
  Other z;
  _State(this.x, this.y, this.z);
}
class Component extends Vue<_State> {
  setup() {
    final mousePos = MouseLogic();
    final other = OtherLogic();
    return _State(mousePose.x, mousePose.y, other);
  }
}

As long as the library user is educated to write component.dart but import component.vue.dart, this approach is doable and very flexible. However, the implementation of this builder might be very costly or even impossible since user-written 'setup' function might be complex enough to make static analysis impossible.

I've been working on a dart port of our javascript/typescript project, and applied this approach. Despite the difficulty of builder implementation, it makes it possible to both reducing the amount of code user needs to write and keeping the public API as consistence with the original one as possible. The migration is still WIP so I cannot tell if there's any pitfall.

refi64 commented 5 years ago

@Nandiin VueDart already used compile-time tweaking, the main issue with that approach is that, without running a full build, none of the code can be type-checked, which IMO isn't my favorite compromise.

nandin-borjigin commented 5 years ago

Hi, @refi64 . Could you lead me to understand why there is a typechecking problem (before build) ? As far as I can imagine, the public interface of reusable hooks would hopefully be comprehensive to be referred to without running any build step, and I think the only type information we need from a component is 'whether it is a Component or not (typical inheritance relationship)' as all logical compositions should be implemented by hooks and component compositions are only for expressing ui hierarchy.

refi64 commented 5 years ago

Consider this same example:

class Component {
  setup() {
    final mousePos = MouseLogic();
    final other = OtherLogic();
    return {
      x: mousePos.x,
      y: mousePos.y,
      z: other
    };
}

If we have a custom render function:

void render(/*???*/ state, ...)

what's the type of our state? We could at best represent it as a Map<String, dynamic>, which means that the values would all be untyped.

This also causes questions such as this:

setup() {
  // ...
  var map = {
    // ...
  };
  map.pop(...);
  return map;  // how do we compile this?
}

Basically, Dart maps are far more untyped than TypeScript's...

nandin-borjigin commented 5 years ago

I must admit that I didn't think of custom render functions and maps.