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

Knockout in combination with 3rd party Web Components leads to messed up HTML #2500

Open
karimayachi opened this issue Sep 24, 2019 · 6 comments

Comments

@karimayachi
Copy link
Contributor

karimayachi commented Sep 24, 2019

Hi,

I've been experimenting with 3rd party Web Components, in order to get rid of the HTML/CSS mess of some layout- and UI-frameworks (you know who they are).

The context
Of course there is a conceptual mismatch between the two. It would be nice to have the Custom Element's properties and attributes to be observable and let Knockout keep everything two-way binded. Alas, most 3rd party WCs are build on LitElement/Polymer or Stencil that come from the world of state machines / one-way-binding and not from our beloved MVVM world with it's two-way-binding.

So, hooking into the attributes and properties with observables will probably be a challenge, especially for 3rd party WCs that are beyond our control. And we're left with our regular "data-bind"-bindings that have Knockout and the WC manipulating the DOM at the same time in order to perform their respective binding-magic.

It's pretty obvious that this will lead to problems, but still.... Simple text-, value- or foreach- bindings should work IMO and I would be happy just to have those working.

But unfortunately, no dice... The foreach-binding messes up most 3rd party WCs.

I've tried:

Both Ionic and Weightless WCs don't work in a foreach binding. They both exhibit the exact same problem (HTML elements are doubled) even though they use different techniques. Material Components WCs don't seem to exhibit this behaviour, but I don't really trust it since they're build on LitElement, just as Weightless.

The problem
When there's a foreach-binding containing WCs, that loads directly on start-up, it works 9 times out of 10 (obvious race condition), but when the foreach is created or updated later on (for instance, when adding items to the binding, when lazy loading a component containing a foreach, or even when just delaying applyBindings with a setTimeout) the created items contain corrupted HTML (elements are doubled in most cases).

My findings
KO's foreach uses the internal templating engine and the childNodes of the bound element are used as template. However, as soon as the WC's javascript has loaded and the browser has manipulated the DOM to reflect the WC's design (added CSS and sometimes even elements to the Light DOM) the KO template engine will use those new nodes from then on as a template. And when inserting new elements the browser will again try to manipulate already manipulated nodes and so they get corrupted.

The solution
If I keep the template out of the browsers reach, It won't be mangled by the WC's JS and the browser and Knockout will inject clean nodes into the DOM.

E.g. this doesn't work (example using Ionic WCs):

<ion-grid data-bind="foreach: list">   
    <ion-row>
        <ion-col size="4">
            <ion-item>
                <ion-label position="floating">Label</ion-label>
                <ion-input data-bind="value: $data"></ion-input>
            </ion-item>
            <ion-button data-bind="click: $parent.delete">Delete</ion-button>
        </ion-col>
    </ion-row>
</ion-grid>

But this does:

<ion-grid data-bind="template: { foreach: list, name: 'item-row' } ">   
</ion-grid>

<script type="text/html" id="item-row">
    <ion-row>
        <ion-col size="4">
            <ion-item>
                <ion-label position="floating">Label</ion-label>
                <ion-input data-bind="value: $data"></ion-input>
            </ion-item>
            <ion-button data-bind="click: $parent.delete">Delete</ion-button>
        </ion-col>
    </ion-row>
</script>

This is however a lot less readable. Especially with many foreach bindings.

So now what?
I understand that there are a lot of conceptual differences between the worlds of Knockout and of Web Components. I don't really know how we can get foreach templates from the DOM without the browser pro-actively manipulating them. Still I see a lot of people online using WCs with Knockout. So, what am I missing here?!
@avickers, you mentioned in #2483 that you moved away from Knockout components to Web Components. How do you deal with this? Or is everyone rolling their own in stead of using 3rd party WCs?

Kind regards,
Karim

@avickers
Copy link

avickers commented Sep 25, 2019

Hi Karim,

I actually have actually been working on my own WC library as a reference implementation. It is themeable, so hopefully not everyone will need to roll their own.

I decided to go ahead and use the native WC APIs rather than one of the polyfilled libraries. WC support in the major browsers is decent. I no longer believed it to be worth the performance costs to use something like LitElement.

I handle templating for foreach bindings differently than Knockout does. In Knockdown, the template is actually defined in the view model via chaining, not in the HTML.

const vm = {
  myList: ko.observableArray(_someData_)
  .template('<li>${firstName} has ${eyeColor} eyes.</li>')
}

You chain the template method after declaring the ObservableArray. You provide the template as a string; however, it will be processed as a template literal, with access to the properties and methods on each item in the array. Alternatively, $data will inject the raw data, as with Knockout.

This can also support using a CE, such as:

const vm = {
  myList: ko.observableArray(_someData_)
  .template('<custom-avatar name=${firstName} src=${imgURL}></custom-avatar>')
}

The library automatically creates and applies the attr binding(s) when it detects attr=${variable} during interpolation. You would simply define any logic for those attributes using the custom element class definition.

I suppose that I could look at adding a special default case, such as:

.template('<custom-element $vm=$data></custom-element>')

Where the VM for each item would be constructed from the raw object data of each array item. That would push all of the markup into the child component definition.

@karimayachi
Copy link
Contributor Author

Hi Andrew,

Thanks for your thoughts!

For rolling my own components I think native WC APIs would have my preference over polyfilled libraries as well, but I still manage to do everything I want with Knockout Components. I use those for everything (dynamically for views and statically as custom elements and everything in between).

I'm really looking at 3rd party WCs here. In my dreamworld I can just pull in any WC (e.g. from NPM) and use those as black box building blocks. They can be created in any form; reactive, state-based, vanilla JS, HTML+jQuery for all I care. Just as long as the implementation is abstracted away in a black box.

<rant>
But I don't want to use those techniques in my business applications. I want to use MVVM and strict separation of concerns, as I think that's still the cleanest form of coding applications that are constantly under development. So I don't want to have to wrap everything in a state-based component made with the same underlying technique of the WCs just to use the WCs.

In before mentioned dreamworld I want to make applications in an MVVM matter, with SoC. Pull in WCs and use (a) Knockout (-type library) to glue everything together.
</rant>

Anyway, maybe this is not the place to discus my wishes for the future of web development. And maybe it's not even an (technical) issue either. I'll leave it open anyway, maybe someone has some more thoughts on this.

Is Knockdown your library? Is it public? I'm curious.

Regards,
Karim

@avickers
Copy link

avickers commented Sep 25, 2019

Hi Karim,

Well, I suppose it's true that the way that I currently make SPAs is kind of a Matryoshka doll of WCs. That's not strictly necessary..

I am in Japan presently without a lot of time to test, but I believe what you want might work with Knockdown.

There is no component binding, but you could simply import the 3rd party components at the top of the module, then use an html binding, and then control them from the main VM with interpolated attr bindings as I describe above.

One major difference between Knockdown and Knockout is that I use MutationObservers and, if you subsequently add new data-binds to the markup after applyBindings is called, they will be automatically applied. (Proper context is maintained by the ShadowDOM(s), which the MO cannot pierce, and the fact that you can set an explicit root element.) That's why something like

import CustomElem from '3rdPartyLib'

const vm = {
  myHTMLbinding: ko.observable('<custom-elem some-attr=${myVar}></custom-elem>'),
  myVar: ko.observable('red')
}

should work. The HTML binding will insert the custom element; it will detect the implicit attr binding and insert it; and, Knockdown will automagically start to process the attr binding when its inserted into the DOM inside of the context of an active view model. If you're doing this inside of a foreach, the example code is a little more complex, but I don't see why it wouldn't work.

The only potential issue is that many of these third party WCs have their own funky syntax when it comes to CE attributes. If you need to add curly braces to use their WCs, then things might blow up. Otherwise, as long as the 3rd party components are self-contained and driven by attributes, I don't see a problem with treating them like a black box.

The libraries are currently private and very much in an alpha state, but I could see about providing you access.

EDIT: One last caveat, not all 3rd party WC libraries really follow the standards, and some of them expect LightDOM styles to be applied to their components.

@karimayachi
Copy link
Contributor Author

Hi Andrew,

Sounds like an interesting approach and it would be great if you could give me access. Maybe it will fit my workflow, maybe it won't, but in the very least it will provide some insights I think....

Regards

@julientype
Copy link

DOM is first party
Knockout is 2d party to dom
Your the 3d party messing up the code....
a 4th party Web Components fight over who controls the dom engine
creating a Knockout push on your web app

@karimayachi
Copy link
Contributor Author

I'm not really sure I understand what you mean. I used "3rd party" as a general term for software that was neither made by me, nor the makers of Knockout.

But since I'm here, I'll might as well answer your other issue :-)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants