karimayachi / karimayachi.github.io

MIT License
2 stars 0 forks source link

Thoughts on Karim's implementation #9

Open karimayachi opened 4 years ago

karimayachi commented 4 years ago

So this is all valid:

<h1>Hello ${name}</h1>

<div>
    It seems your age is ${age} and your favorite genres of music are:

    <ul @foreach="genres">
        <li>${this} <a href="#" @onClick="genres.deleteGenre">[x]</a></li>
    </ul>
</div>

<div>
    <input type="text" @value="name"></input>
    <input type="range" @value="age"></input>
    <button @onClick="reset">Reset!</button>
    <button @onClick="addGenre">Add genre</button>
</div>

While working on this, I had two new ideas. I haven't discussed them before on the repo, because I just now thought of them. But I implemented (one of) them anyway, because I'm very pleased with them :-)

1. Named Scopes

I never liked the $root and $parent and $parents[] placeholders, as it becomes unreadable and messy quick. I also didn't feel like implementing a hierarchy of binding contexts (I keep the element-viewmodel bindings in a WeakMap, not in DOM as KO does, so there's no inherent hierarchy -with slow lookups-). So I came up with "Named Scopes". Basically you can 'tag' a level in your DOM hierarchy and that tag now identifies the viewmodel behind that DOM node. Also certain bindings can automatically create a Named Scope (for now only foreach which creates a 'tag'/Named Scope with the name of the bound viewmodel-property).

So instead of:

<div @with="selectedPerson">
    It seems your age is ${age} and your favorite genres of music are:

    <ul @foreach="genres">
        <li>
            ${this}
            <a href="#" @onClick="$parent.deleteGenre">[x]</a>
            <a href="#" @onClick="$parents[0].showGenre">[show all people with this taste]</a>
        </li>
    </ul>
</div>

it becomes:

<div @with="selectedPerson" @scope="people">
    It seems your age is ${age} and your favorite genres of music are:

    <ul @foreach="genres">
        <li>
            ${this}
            <a href="#" @onClick="genres.deleteGenre">[x]</a>
            <a href="#" @onClick="people.showGenre">[show all people with this taste]</a>
        </li>
    </ul>
</div>

Of course I would still have to figure out how to deal with duplicate names.

2. Method binding

This I haven't implemented yet, because I just thought of it. But if we have two-way binding of WC-properties and binding of event-handlers, why not binding of methods? This would be reminiscent of the good old RelayCommand from WPF and the idea of ViewModel Outlets in iOS (at least when they still had the Interface Builder -- probably doesn't exist anymore. So, next to '@' and ':' we would have another symbol, say'+' for method binding. Consider this Snackbar WC from material design web components:

<mwc-snackbar @labelText="errorMessage" +show="openSnackbar" +close="closeSnackbar"></mwc-snackbar>

ViewModel:

class ViewModel {
    @observable errorMessage: string;
    openSnackbar?: Function;
    closeSnackbar?: Function;

    constructor() {
        this.errorMessage = '';
    }

    fetchData = (): void => {
        try {
            //do something
        }
        catch
        {
            this.errorMessage = 'We tried something.. it failed';
            this.openSnackbar!();
        }
    }
}
karimayachi commented 4 years ago

From Andrew:

Not being a .NET guy, I'm not completely sure that I understand RelayCommand. You would have an openSnackbar Observable with a Boolean, and if it ever became true, then it would call show() on the web component?

I looked into the spec around attribute names for custom elements

Any namespace-less attribute that is relevant to the element's functioning, as determined by the element's author, may be specified on an autonomous custom element, so long as the attribute name is XML-compatible and contains no ASCII upper alphas. The exception is the is attribute, which must not be specified on an autonomous custom element (and which will have no effect if it is).

It seems that to be truly spec compliant the attribute names should follow XML naming conventions and begin them with a lowercase letter. I guess that's why Vue directives begin with "v-" and Angular directives begin with "ng-". I'm not sure that any browsers would actually misbehave. Do people lint HTML? I kind of like the ampersands, but it is a risk.

karimayachi commented 4 years ago

Not being a .NET guy, I'm not completely sure that I understand RelayCommand.  You would have an openSnackbar Observable with a Boolean, and if it ever became true, then it would call show() on the web component? 

Not exactly and maybe my RelayCommand analogy wasn't really a good one. It's more of an inverted RelayCommand. RelayCommand lets you bind actions (Commands) in a UI-component (say a click on a button) to methods in your ViewModel. So clicking a button would trigger your DoSomething in the ViewModel. Of course we have the click-binding for that (although it works by catching the event). My proposal would do the inverse: it would relay a method call on your ViewModel to a method on the WC. It would alias the WC-method on the ViewModel.

If the WC exposes a boolean we could bind with the usual property binding as you said:

<mwc-snackbar id="errorSnackbar" @opened="snackbarOpened"></mwc-snackbar>
someFunction = (): void => {
    this.snackbarOpenend = true;
}

But this is semantically weird and many times the WCs do not even expose a boolean property but rather a method, like this Snackbar exposes show() and close() methods.

In KO you have no other option than to tightly couple the VM and the HTML, either by capturing the element (with a custom binding or something) or by querying the DOM:

<mwc-snackbar id="errorSnackbar" data-bind="customElementBinding: snackbarElement"></mwc-snackbar>
snackbarElement = ko.observable();

someFunction = (): void => {
    this.snackbarElement().show();
}

or

<mwc-snackbar id="errorSnackbar"></mwc-snackbar>
someFunction = (): void => {
    document.getElementById('errorSnackbar').show();
}

The first option introduces DOM elements in the ViewModel. The second option creates a tight coupling between VM and V (relying on id's etc)...

So my proposal is to create a binding for methods that 'aliasses' the method on the WC to the ViewModel:

<mwc-snackbar +show="openSnackbar"></mwc-snackbar>
someFunction = (): void => {
    this.openSnackbar();
}

It seems that to be truly spec compliant the attribute names should follow XML naming conventions and begin them with a lowercase letter. I guess that's why Vue directives begin with "v-" and Angular directives begin with "ng-". I'm not sure that any browsers would actually misbehave. Do people lint HTML? I kind of like the ampersands, but it is a risk.

Yep, I was afraid that it wouldn't be compliant. I really like the readability of it though. But this is just a detail and we can always look at the specifics later. Maybe even make it optional by having a 'strict-mode' or something. edit: : and _ are allowed as start character.

avickers commented 4 years ago

Great work, Karim!

Some quick observations until I have time to play around with it.

1) I think the onClick binding should pass the event through.  I know there were a few times that I needed the exact coords when developing KO apps.

2) Should we consider bindings for touch events?

3) I nominate that we rename the "with" binding to either "context" or "scope" because otherwise I don't think it's fluent enough to make any sense to a newbie without consulting the docs.

4) As for the Relay and Property bindings, I'm a little leery. Web Components can be used two ways: declaratively and imperatively. The declarative way to use them is to use attributes and treat those as the public API. The imperative way is to get a reference and access properties and methods directly; however, this potentially promotes tighter coupling as you highlight.

I understand that you wanted to address this with the Relay, but rather than:

<mwc-snackbar @labelText="errorMessage" +show="openSnackbar" +close="closeSnackbar"></mwc-snackbar>

All you need to do is use an attribute binding bound to "open".

<mwc-snackbar @labelText="errorMessage" open=${isOpen}></mwc-snackbar>

Where "isOpen" is an Observable with a Boolean value. The Snackbar component will automatically reflect changes to the open attribute to call show() and close(). Even if you wanted to switch out with another snackbar/toast component, the general declarative approach probably won't change a whole lot.

<wl-snackbar open=${isOpen}></wl-snackbar>

<ko-toast show=${isOpen}></ko-toast>

It feels like you're trying to make the imperative approach declarative. Is creating a special hybrid style really necessary?

5) Re:

One thing I noticed: usually in KO in an SPA, I have my views, partial views and major chunks (the intermediate abstraction level) implemented as KO-components (that are either packaged or lazy loaded with RequireJS or something like that). Of course I couldn't do that here, so I just fetched some html and put it in the viewmodel's html property. It occurred to me how close this is to Knockdown,

Your latest email and demo have caused me to imagine (:smile:) a potential solution to the marriage between MVVM and Composition that we've been circling around for the past month. I think it's a great compromise that will bring the best of both approaches and tie everything together neatly! I'll work on prototyping it before putting the cart too far ahead of the horse...

karimayachi commented 4 years ago

I think the onClick binding should pass the event through. I know there were a few times that I needed the exact coords when developing KO apps.

Yes, good point. The onClick binding should be a generic event-binding that passes the event and the viewmodel/context.

Should we consider bindings for touch events?

Yes, a generic event-binding should cover that as well.

I nominate that we rename the "with" binding to either "context" or "scope" because otherwise I don't think it's fluent enough to make any sense to a newbie without consulting the docs.

I like context... Scope is already reserved for my "Named Scopes". Although those could be renamed to "Namespaces"

As for the Relay and Property bindings, I'm a little leery. Web Components can be used two ways: declaratively and imperatively. The declarative way to use them is to use attributes and treat those as the public API. The imperative way is to get a reference and access properties and methods directly; however, this potentially promotes tighter coupling as you highlight.

I think imperative programming should be considered an anti-pattern 😄. I totally agree that the attributes should be treated as the public API for WCs. However, I think that extends to properties as well. Maybe even more so. I find myself constantly looking at the WPF way of doing things. In WPF attributes only serve a syntactical purpose. Since the markup is compiled and doesn't exist run-time, attributes are only used to bind the control-properties to the viewmodel-properties. The same goes for a property-bindings in Imagine: attributes are used to set things up, but are not observed. I think attributes are way to crippled to be the primary public API: they only take (serialized) strings and are only one-way, unless we inject the overhead of Mutation Observers. I think (but this is an assumption), that WC properties were meant to be the real public API. That's why developers are encouraged to reflect attributes to properties and vice-versa. I think they fit declarative programming in JavaScript just as nicely as they did in WPF.

The same can be said for method-properties, although I share your concerns. I'm not sure. As per your example: if a WC exposes a boolean attribute/property (sayopen), it would be fine to bind your isOpen property to it.. But what if the WC only exposes the show() and close() methods? Would there be a downside to relaying methods?

every special syntax we create add cognitive load to the framework; and, is one less thing we can add later on as the front end/project evolve;

Ok, let's forget Relay bindings for now.

Your latest email and demo have caused me to imagine (😄) a potential solution to the marriage between MVVM and Composition that we've been circling around for the past month. I think it's a great compromise that will bring the best of both approaches and tie everything together neatly! I'll work on prototyping it before putting the cart too far ahead of the horse...

I'm very curious! Is this solution the hybrid approach you mention here?