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

Get access to the element of a component #1483

Closed
andersekdahl opened this issue Aug 13, 2014 · 6 comments
Closed

Get access to the element of a component #1483

andersekdahl opened this issue Aug 13, 2014 · 6 comments

Comments

@andersekdahl
Copy link

Hi!
We're using the new (and excellent) knockout components, and came across a situation where we need to access the DOM element for the component. But the element seems to only be passed when using the createViewModel method, and not when viewModel is a method. I was wondering what the reasoning behing this is? I'm not really sure what the difference is between using viewModel and createViewModel, is viewModel just a shorthand for createViewModel? In that case, it seems like it should get the DOM element.

@3cp
Copy link

3cp commented Aug 15, 2014

@SteveSanderson
Copy link
Contributor

We may want to ensure the docs are clear on this, but it is definitely as designed.

The reasoning is that, almost always, people shouldn't be interfering with the element directly from the viewmodel (it undermines the whole MVVM thing) - this is what bindings are for instead! It's deliberately behind an extra layer of abstraction, though remains possible via createViewModel if you really want it :)

@andersekdahl
Copy link
Author

Good points, but I think a lot of the times you create a custom element/component to wrap DOM stuff, not just to create large components that call bindings. I'm guessing some components won't even have their own view models, they will only acts as wrappers around a template and DOM mutations.

@richotaylor
Copy link

I needed this as well. I love the new components feature and wanted to use it to load various sections of our medium / large scale SPA. I've found a workaround that seems to be working so far.

Explanation: While all sections of our page could be adapted into the component model, not all of them also wanted to use knockout. Some were already coded with knockout, calling applyBindings internally, etc. while other sections need to NOT have knockout at all. Those latter sections either already have a another data binding solution or are doing something very specific and don't want any data binding. These components want full control over their rendering and their dom element in general. That's why they need access to the dom element, sort of like Backbone or this.$. in Polymer (if I understand correctly.

For the moment I have pieced together a loader that injects the element into the View Model's constructor. Hopefully not too fragile with respect to future releases of KO.

ko.components.loaders.unshift({
    // Could this be accomplished cleaner with loadComponent?
    loadViewModel: function (name, viewModelConfig, callback) {
        if (typeof viewModelConfig === "function") {
            var viewModelConstructor = {
                createViewModel: function (params, componentInfo) {
                    return new viewModelConfig(params, componentInfo.element)
                }
            };
            ko.components.defaultLoader.loadViewModel(name, viewModelConstructor, callback);
        } else {
            callback(null);
        }
    }
});

Also created the typical "stopBinding" binding:

ko.bindingHandlers.stopBinding = {
    init: function () {
        return { controlsDescendantBindings: true };
    }
};

My component that wants to be loaded but doesn't actually want to use KO internally. Wants total control

define(function (require) {

    var wrapperTemplate = require('text!./comp-non-ko-wrapper.html'),
        template = require('text!./comp-non-ko.html'),
        $ = require('jquery')
        _ = require('underscore');

    var ViewModel = function (params, element) {
        this.$el= $(element).find('.wrapper');
        this.template = _.template(template);
        this.render();
    }

    ViewModel.prototype.render = function () {
        // Render my custom, non ko template
        this.$el.html(this.template());
    }

    return {
        viewModel: ViewModel,
        template: wrapperTemplate
    };
});

My component / wrapper template (notice the stopBinding):
comp-non-ko-wrapper.html

<div data-bind='stopBinding' class='wrapper'>
</div>

My actual template (data-bind is intentionally ignored, that's the test)
comp-non-ko.html

<!-- (Just making sure these bindings aren't picked up) -->
<div>Hello <span data-bind="text:name"></span>
</div>

@TheHans255
Copy link

I know that this thread is old, but a point I want to bring up as far as design is that there are some cases where Knockout components need to have very complex visuals that cannot be controlled simply (or efficiently) from view model properties, yet also do not make sense to have within the global scope of custom bindings since they won't be used outside of that component. A pertinent example would be using any of d3.js's SVG visualizations. This issue could probably be resolved without having to break the MVVM design pattern if we allowed component-level definitions of custom bindings.

@shawty
Copy link

shawty commented May 4, 2016

One VERY GOOD use case that I'm currently trying to solve, where access to the parent element would be desirable is this one:

I have a toolbar, and this tool bar can have injected into it a number of different components (Pesudo code)

<toolbar>
  <icon></icon>
  <icon></icon>
  <singlespace></singlespace>
  <menutab></menutab>
  <menutab></menutab>
  <menutab></menutab>
  <spaceconsumer></spaceconsumer>
  <icon></icon>
  <icon></icon>
</toolbar>

In my specific case, inside the markup for my toolbar I have the following:

<div class="menubarContainer" data-bind="foreach: menubarComponents">
  <div data-bind='component: { name: controlType , params: $data}'></div>
</div>

As you can see this is a very standard looped binding, which results in one div for each component, defined in the 'menubarComponents' observable array.

As it stands, it works and it works well in general.

However, I (and I suspect many others) have been using the new 'flexible box' model a lot more recently.

Flexbox greatly simplify's complex layouts, and you can for instance add the following styling to a toolbar:

.menubarContainer{
  display: flex;
}


and that then automatically put's ANY divs contained inside that div, under flex item control.

again, in my specific case, I'm trying to do the following

<menubar style="display: flex;">
  <icon></icon>
  <icon></icon>
  <singlespace style="flex: 0 0 40px;"></singlespace>
  <menutab></menutab>
  <menutab></menutab>
  <menutab></menutab>
  <spaceconsumer style="flex: 1;"></spaceconsumer>
  <icon></icon>
  <icon></icon>
</menubar>

Notice the styles on some of the elements this time?

The single space component will take up exactly 40 pixels of usable space, creating a 40px space between the icons and menu tabs, while the 'space consumer' will use up as much space as is left after that 40px plus the sizes of all the other components have been deducted.

The upshot is, you end up with a very well laid out toolbar, that has 2 icons at the left, 40px of space, 3 text based tabs, then 2 icons right to the right of the bar with properly balanced space between them and the left elements.

again, this all works wonderfully, but alas here's the sting in the tail....

when you define "display: flex" on a parent element, the flex child layout ONLY applies to immediate children, which in the case of this post means the div with the data-bind containing the component on it. (The display flex is on the div that has the for-each)

Since I would like to control the flex box sizes and spacing from the components I'm inserting (The idea is that I can configure my reusable menubar by defining an array of components, most likley either loaded via json or passed in the params) , we ideally need a way to be able to say, here is my element, please add this css style rule on it's container.

I've gotten around this in some cases, where the flex container has had an ID on it, and I've been able to use '#id > div.blah' in the parent, but in the case I've highlighted, I can't just slap an extra rule on the component div because then that would apply to every lower level component that might get inserted into the toolbar.

So, in summary, even if we cannot have a way of getting at the parent element (and I do understand the reasons for this) would it not at least be an idea to provide a mechanism where by our components can at least apply some css rules to it.

Shawty

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

6 participants