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

Support prerendered foreach content #1702

Open
mariusGundersen opened this issue Feb 19, 2015 · 22 comments
Open

Support prerendered foreach content #1702

mariusGundersen opened this issue Feb 19, 2015 · 22 comments

Comments

@mariusGundersen
Copy link
Contributor

Most of knockout is great for having the server render the content, and then updating it when the JS loads on the clientside. This ensures that the page isn't blank while the JS is loading, but has visuals right away.

Unfortunately this doesn't work very well with foreach, since it will repeat everything inside the element with the foreach binding. If the server uses server side templating to render a list of n items, then the client will render a list n*n items. This means that lists will have to wait to be rendered until the client has fully loaded.

A potential fix for this would be to have the foreach binding recognize an attribute on dom elements and only create its template from the ones with the correct attribute. For example, it could look for a data-ko-foreach-template attribute and only use elements with that attribute when creating its template. In the following case only the first <li> will be used as the template for items inside the foreach. With server side rendering the first element could be marked with this attribute when outputting the list.

<ul data-bind="foreach: list">
  <li data-ko-foreach-template>First</li>
  <li>Second</li>
  <li>Third</li>
</ul>

This is only a very simple solution though, and I believe a better solution would be to have an attribute that indicates the index, for example data-ko-foreach-index="0". It is easier for a server side templating language to output the index for every element than to output an attribute for only a single element. Knockout would then have to group the dom elements by index and create templates from them. This way it would be possible for knockout to reuse the existing DOM elements the first time it renders the list, similar to how it reuses existing DOM elements when rerendering a list. The example above would look like this:

<ul data-bind="foreach: list">
  <li data-ko-foreach-index="0">First</li>
  <li data-ko-foreach-index="1">Second</li>
  <li data-ko-foreach-index="2">Third</li>
</ul>

In the scenario where no element has a data-ko-foreach-index attribute then the entire contents of the element will be used to create the template.

@vamp
Copy link

vamp commented Feb 19, 2015

I think that the best solution for your case would be using of templates, or: https://github.com/mbest/knockout-switch-case

@mariusGundersen
Copy link
Contributor Author

I don't see how that would work @vamp.

I've been looking into this, and I think it should be possible to do this in the template engine, without touching the "core" of Knockout.

@SteveSanderson
Copy link
Contributor

I'd be interested in what other KO team members think. Personally I agree it would be cool to have such support, but am a bit nervous about going partially down the 'bind to prerendered content' route.

The most general solution would be making KO able to attach to arbitrary prerendered content (not just with foreach, but with all control-flow bindings, components, etc.), but that's extremely complex and I'm not sure we'd get to that without some major restructuring of how things work. Just supporting foreach would be beneficial in some cases, but has the risk of confusing developers into architecting with the expectation that they can do this everywhere.

It's possible we might at some point do this, but I'm not expecting it to happen imminently.

@mariusGundersen
Copy link
Contributor Author

I've been looking into this a bit, but haven't had the time to make a prototype yet. Currently it is only foreach that is an issue, since all other bindings (like text, value, html, template and if) replace whatever is there already. The only problem I see is with components, and I don't have enough experience with components to tell where the difficulties might be.

So currently it is quite easy to render the page server side and then have Knockout take over on the client. Except for lists and components (and ifnot, but I don't see how that can be solved).

I'll look into this a bit more for foreach

@ErikSchierboom
Copy link

Support for pre-rendered content would be great! I have been looking into this lately, and getting the text and value binding to work with existing content can be done in a fairly straightforward way. I have a create a JSFiddle that shows a very simple example of a textDefault binding that automatically initializes an observable with the HTML element's innerText and then applies the text binding: http://jsfiddle.net/eyz3hsbu/

Unfortunately, getting the foreach binding to work has been an uphill battle so far. This is mainly due to the fact that the foreach binding is much more complex and my KO knowledge only goes so far.

If someone (e.g. @mariusGundersen ) manages to get a prototype working, I'd be extremely interested in seeing it!

@NoelAbrahams
Copy link

@ErikSchierboom, my solution was to add a hidden reference to the DOM element to each of the for-each items.

ko.bindingHandlers['prerendered-for-each'] = (function () {

      function mapping(arrayItem) {

          var htmlElement = arrayItem.__hiddenDom;

            if (!htmlElement) {
                throw new Error('MissingElement');
            }

            return [htmlElement]; // An array of HTML elements is required
        }

        return {

   update: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {

                ko.utils.setDomNodeChildrenFromArrayMapping(
                    element, // UL element
                    ko.utils.unwrapObservable(valueAccessor()), // array reference
                    mapping // DOM node provider for each array value
                    );
            }
        };
})();

Obviously this is an ad-hoc solution and it will be great to have library support for this.

Edit:

It's important to ensure the DOM reference is disposed as appropriate.

@mariusGundersen
Copy link
Contributor Author

@ErikSchierboom, where I work we tried to have an initValue custom binding on input fields, but quickly found that it makes it very difficult to see what observables will be initialized when the viewmodel is created. The way we do it now in DecoJS is to have a model and viewmodel attached to the html, for example (using the .net razor syntax):

<div data-viewmodel="SomeVM" data-model="@Json(Model)">
  <span data-bind="text:name">@Model.Name</span>
</div>
define(['knockout'], function(ko){
  return function SomeVM(model){
    this.name = ko.observable(model.name);
  }
});

The content of the span is rendered with the model name, and then the content of the span is replaced when the viewmodel has loaded. This works for most bindings, like text, value, attr, visible, etc, but not for foreach, and so Knockout handles most pre-rendered content pretty well.

@ErikSchierboom
Copy link

I adapted the fastForEach code and came up with a small library to allow the text, value and foreach binding handlers to be initialized from existing DOM elements: https://github.com/ErikSchierboom/knockout-pre-rendered

It allows you to do the following:

function ViewModel() {
  this.name = ko.observable();
}```

```html
<span data-bind="textInit: name">Michael Jordan</span>

This will result in the observable being initialized with the innerText value of the bound element, after which the regular text binding handler is applied.

A similar handler has been added for the value binding handler:

<input data-bind="valueInit: name" value="Michael Jordan" type="text" />

The most hard part was the foreach binding handler, for which I adapted the fastForEach code of Brian Hunt.

Consider the following view model:

function PersonViewModel() {
  this.name = ko.observable();
}

function ForeachViewModel() {
  this.persons = ko.observableArray();

  this.persons.push(new PersonViewModel());
  this.persons.push(new PersonViewModel());
  this.persons.push(new PersonViewModel());
}

You can bind the array using the following HTML code:

<ul data-bind="foreachInit: persons">
  <li data-template data-bind="text: name"></li>
  <li data-init data-bind="textInit: name">Michael Jordan</li>
  <li data-init data-bind="textInit: name">Larry Bird</li>
  <li data-init data-bind="textInit: name">Magic Johnson</li>
</ul>

It is not the most elegant solution, but it works. If people have suggestions I'd gladly receive them!

@brianmhunt
Copy link
Member

@ErikSchierboom Cool!

One thought: the order of bindings is topologically stable (i.e. they're run in-order, e.g. #950), so instead of extending existing bindings you add a new binding that precedes the others:

<span data-bind="initFromText: name, text: name">Michael Jordan</span>
<span data-bind="initFromText: name, textInput: name">Michael Jordan</span>
<span data-bind="initFromText: name, value: name">Michael Jordan</span>

The initFromText will always be processed before the remaining bindings. You can immediately see how the separation of initialization and modification makes the binding more reusable elsewhere. The code for it should be trivial e.g.

ko.bindingHandlers.textFromInput = {
  init: function(element, valueAccessor) {
     valueAccessor()(element.textContent || element.innerText)
  }
}

If going down this road, you could also have a initArrayFromList or some such, that pairs with foreach or fastForEach. Obviously that's a bit more involved, but it feels like the technique above for initFromText could have a list-equivalent.

Just some food for thought. 🍻 Thanks for advancing this issue!

@brianmhunt
Copy link
Member

Another cool option may be to look at preprocessing. Again, just food for thought – it just might save a bit of code. :)

@ErikSchierboom
Copy link

@brianmhunt Excellent ideas! I never considered the bindings to be topologically stable, that is a neat trick. I'm not sure how that would work with a foreach or fastForEach, because what I would really want is to not re-render the elements in the foreach loop. That would be impossible right?

The pre-processing also looks very interesting.

I have some work to do :) Thanks a lot for all the tips!

@brianmhunt
Copy link
Member

My pleasure, @ErikSchierboom

It's not entirely impossible to avoid re-rendering, but it would take a lot of nuanced work. You would likely have to delve into the foreach/fastForEach binding being used. It is doable, but non-trivial. I feel like it would take some design thought to make a binding that is generic.

The easiest way strikes me as:

  • slurp the data
  • populate the observable array
  • remove the non-template elements from the DOM
  • bind with foreach / fastForEach

🍻

@PaulHuizer
Copy link

slurping the data while populating the observable array with child nodes and so on, seems the difficult part here. Would an extra (not knockoutjs) data-slurp attribute on elements be helpfull for a generic method?

Alternative thought:
Would it not be simpler to have the server generate the html 2 times?
The first version of the HTML contains the data .. so the webpage instantly looks good.
The second version of the HTML contains the knockout version of the HTML

While the page initialises as normal, data arrives from the server .. enabling knockoutjs to render the second part of HTML. Then simply remove the first part?

@ErikSchierboom
Copy link

@brianmhunt I am looking into pre-processing. Am I right that it does not provide access to the element being bound? If so, how would I use it?

@brianmhunt
Copy link
Member

@ErikSchierboom - You've a couple options.

If you are adding a preprocessor to a bindinghandler, you are just given the string e.g. ko.bindingHandlers.textInput.preprocess is passed the string. You can parse it or use e.g. ko.bindingProvider.instance.getBindingAccessors or some such to get a list of bindings and modify them. That's hackey. You could just prepend text like this:

ko.bindingHandlers.textInput.preprecess = function (bindingString) {
  return "initFromText: value, " + bindingString
}

But again that's kinda hackey, and you'd need to either be very consistent or parse bindingString to find out what the variable name for value should be.

A better or at least alternative option may be ko.bindingProvider.instance.preprocessNode, which is given the node. From the docs:

If defined, this function will be called for each DOM node before bindings are processed. The function can modify, remove, or replace node. Any new nodes must be inserted immediately before node, and if any nodes were added or node was removed, the function must return an array of the new nodes that are now in the document in place of node.

Not sure either is a silver bullet for anything, but they did cross my mind as opening up possibilities for thinking about at least. 😁

@ErikSchierboom
Copy link

Thank a lot again. I will look into it further.

@mbest
Copy link
Member

mbest commented Apr 22, 2015

When using the binding handler preprocessor and you want to add another binding, there's a standard way to do so:

ko.bindingHandlers.textInput.preprocess = function (value, name, addBindingCallback) {
    addBindingCallback('initFromText', value);
    return value;
}

@brianmhunt
Copy link
Member

@mbest - Awesome, I didn't know about addBindingCallback! :)

The would work only if/because addBindingCallback prepends the new binding to the stack of bindings, which it looks like it does by recursing depth-first, unless I am mistaken. Please correct me if I am wrong here.

@mbest
Copy link
Member

mbest commented Apr 22, 2015

The would work only if/because addBindingCallback prepends the new binding to the stack of bindings, which it looks like it does by recursing depth-first.

That is correct. But if you want to ensure that bindings run in the correct order, you could use the after property of the binding handler.

ko.bindingHandlers.textInput.after = (ko.bindingHandlers.textInput.after || []).concat('initFromText');

Although the after property is used by Knockout's built-in bindings, it's not currently documented or "supported", so you'd use it at your own risk.

@ErikSchierboom
Copy link

Correct me if I'm wrong, but would using the preprocessor not be an all or nothing approach? That is, it will be applied to all elements?

I worked some more on my library and managed to combine my textInit, valueInit and init binding handlers into a single binding handle by using the topologically stable property of the binding handlers:

<span data-bind="init, text: name">Michael Jordan</span>
<input data-bind="init, value: name" value="Larry Bird" type="text" />
<span data-bind="init: { convert: parseInt }, text: height">198</span>
<!-- ko init: { field: name } -->Michael Jordan<!-- /ko -->

You don't even need to explicitly specify the observable, it can infer that from the accompanying text, textInput or value binding.

@tadjik1
Copy link

tadjik1 commented Jul 21, 2015

My previous issue was referenced with this, but I'm not sure that I talked about the same thing as you =)
So, my problem is: I have a large SPA with many pages and components, with routing and fetching data from REST API. It takes about 10 seconds for my application starts and it's pretty much. I want to create server rendering solution for knockout or take made.

So I want to get analogue for React.renderToString() but for knockout.js. For example ko.renderToString() instead of .applyBindings().

@brianmhunt
Copy link
Member

@tadjik1 Thanks. Is there not a (fake) DOM in node.js? If so, you could just use

>>> ko.applyBindings(viewModel, node) 
>>> node.innerHTML

If there's a reasonably complaint DOM, Knockout will work. I would enjoy documenting this if you (or anyone) achieves any success.

@mbest mbest modified the milestone: Not assigned Dec 2, 2016
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

10 participants