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
Comments
I think that the best solution for your case would be using of templates, or: https://github.com/mbest/knockout-switch-case |
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. |
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 It's possible we might at some point do this, but I'm not expecting it to happen imminently. |
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 |
Support for pre-rendered content would be great! I have been looking into this lately, and getting the Unfortunately, getting the If someone (e.g. @mariusGundersen ) manages to get a prototype working, I'd be extremely interested in seeing it! |
@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. |
@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 |
I adapted the fastForEach code and came up with a small library to allow the 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 A similar handler has been added for the <input data-bind="valueInit: name" value="Michael Jordan" type="text" /> The most hard part was the 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! |
@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 ko.bindingHandlers.textFromInput = {
init: function(element, valueAccessor) {
valueAccessor()(element.textContent || element.innerText)
}
} If going down this road, you could also have a Just some food for thought. 🍻 Thanks for advancing this issue! |
Another cool option may be to look at preprocessing. Again, just food for thought – it just might save a bit of code. :) |
@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 The pre-processing also looks very interesting. I have some work to do :) Thanks a lot for all the tips! |
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 The easiest way strikes me as:
🍻 |
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: 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? |
@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? |
@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.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 A better or at least alternative option may be
Not sure either is a silver bullet for anything, but they did cross my mind as opening up possibilities for thinking about at least. 😁 |
Thank a lot again. I will look into it further. |
When using the binding handler preprocessor and you want to add another binding, there's a standard way to do so:
|
@mbest - Awesome, I didn't know about The would work only if/because |
That is correct. But if you want to ensure that bindings run in the correct order, you could use the
Although the |
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 <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 |
My previous issue was referenced with this, but I'm not sure that I talked about the same thing as you =) So I want to get analogue for React.renderToString() but for knockout.js. For example ko.renderToString() instead of .applyBindings(). |
@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. |
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 listn*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.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: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.The text was updated successfully, but these errors were encountered: