Hi, I'd like to know if there is a way to do something similar to what StimulusJs does.
I mean: decorate the DOM and tell KnockoutJs to automatically call ko.applyBindings()
to that element.
Since I'm not building a SPA, I already have most of my HTML and just want to make it a little bit more "reactive". I can't call ko.applyBindings()
for each page inside <script>
either, because my ViewModels are not exposed to the page directly from my Webpack bundle.
PS.: I'm aware of this solution, which seems abandoned, and I don't know what are the implications to the way Knockout works. So I just want to know if there is any native resource (or common solution) from KnockoutJs I could use or I'd have to just scan my DOM and bind each ViewModel manually.
I would look at designing your stuff to run as knockout components that you can apply to existing html nodes.
Then you only need one dummy apply binding with no view model for the whole page, i.e.
ko.applyBindings(document.getElementById("#body");
Knockout components can all have separate isolated view models so you can avoid needing to call apply bindings on multiple elements with their own view model contexts.
You can use existing elements in registering your component as long as your register component code runs after the dom node is available (put scripts in footer or use jquery document ready).
Well, if you want a hacky solution that should be quick and dirty, create a promise inside the script tag on the html pages. Expose the resolve callback as a global variable on the window object. When the view model is done loading in the webpack bundle, resolve the promise externally via the global callback, and using then
run apply bindings from the page.
Be forewarned, if your code is reviewed, you'll probably get fussed at for externally resolved promises and need to use some appropriate event based interface instead.
@ryios How would that work if I'm not building a SPA? I mean, I already have all the HTML rendered, so I'm not sure components would fit this specific scenario. Could you explain a little bit more about this alternative you are proposing?
@avickers I've tried having a custom data-vm="ViewModelName"
tag placed somewhere in the HTML and calling ko.applyBindings()
in the element which this tag was found (after the DOM was ready).
But I suppose this would not be as scalable as explicitly calling the binding function. I guess it could be difficult to maintain in the long run with partials views and other MPA techniques.
Ignore me, I'm dead wrong on using existing html.
You can use existing html for components, but it becomes a template and will be cloned into places where you use component binding.
Am really curious though, so digging into alternatives.
@ryios Tell me if you find a good solution.
The whole reason I'm doing this in the first place is because of the way Webpack works.
Usually, I have one js bundle for my whole web app, whose modules I can simply call/ initialize in the page itself. What is getting in my way about Webpack is that I can't require bundled modules in HTML and I haven't found the right configuration to do it. Also, the default tree shaking behavior, that only bundle files that I've explicitly imported is a big issue for my traditional workflow.
So, the closest I've got to make this work was having a meta tag in my layout page <meta name="ko:vm" content="viewModelName" />
which I can bind when DOM is ready using the defined view model name.
In my specific scenario (since I'm rendering everything server-side with Razor); I have a section (@RenderSection("KnockoutMetaTags", false)
) in my layout's <header></header>
, and in the page that uses that layout, I render the tag at the end of it, with possible custom parameters in data-*
attributes for initialization (that I can easily pass through, using information from my Razor view models).
I'm thinking about having a plugin in knockout, so you could end up with something like this:
var myModelInstance = ko.autoBinder.registerModel('my-model-key', myModelHere, modelsToPassToMyModelHere.....);
var loginFormModelInstance = ko.autoBinder.registerModel('login-model-key', loginFormModel, myModelInstance);
````
````
the autoBinder plugin would track all the models you've made, and automatically handle ioc into create new models when registerModel is called.
It would come with a binding handler that would handle "ApplyBindings" for you on the models you've already registered.
The main issue with the autoBinder binding idea is it wouldn't run because no ApplyBindings has been called yet.
Easy thing would be to move it to it's own data- attribute, like "data-auto-bind" or something like that and have it go straight to autoBinder as they're added to the dom... might be kind of heavy though...
Then in your code, you could design your models as individual objects that each call ko.autoBinder.registerModel on themselves.
I don't really run into webpack issues, because I don't use it. I use Gulp 4 and gulp-include, gulp-sass, etc etc. No real reason to switch imo, don't care about tree shaking, I don't have any code in my js that isn't used.
Gulp 4 is really nice and really fast, and I loathe webpack configs. I like the flexibility i have with gulp 4 and I love that I don't need to do "gulp.task" stuff anymore. I can just write basic functions and export what I want and via object alias's I don't have to do "gulp.dest" I can just do "dest" and run-sequence, etc etc is all built in now via "series" and "parallel" and even "watch".
You can do stuff like this:
````
function build(done) {
function buildScripts(sDone) {
return dest...;
}
function buildStyles(stDone) {
return dest...;
}
return parallel(buildScripts, buildStyles)(done);
}
````
So you can run tasks in a task grouping them up so when I export it
````
exports.build = build;
````
as opposed to
````
exports.build = parallel(buildScripts, buildStyles);
````
I like having functions related to the task inside of that task so I can isolate them and use code collapse features.
Long rant, but I really like Gulp 4.
@ryios Well, I have to confess that I thought about using a plugin as you described, I even linked a lib that uses this approach in my original question - I didn't get to test it though; since it's beta and the last commit is 8 years old. I'm not sure what would be collaterals of using this approach extensively, besides of course, what you already mentioned.
Regarding Gulp 4, I was planning to replace it with Webpack, because of the same reason you pointed out (I also dislike the configurations). In the past, I used to use Brunch, which is a lot simpler and solved most of my problems (But the project is kinda dead now).
Since I dislike the "new" trends of development regarding web apps (mostly heavy Js and SPA). I'm finding it very difficult to adapt these opinionated tools to my workflow (I'm more of an MPA + Server Side + Progressive Enhancement kinda guy). That's one of the reasons I'm falling back to Knockout because it's not opinionated how I can build my UI (and I'm following TKO development too).
One thing for sure I have to thank you, I didn't know about this gulp-include
package. I love how Sprockets simply works for Ruby on Rails and I'm very fond of this workflow.
I think what would be really cool is if you could use the component binding on an existing element and have that elements html be the template for that instance of the component. In which case the component binding wouldn't clone or inject the template it would just bind itself to that element and it's children.
I.e. let us leave "template" null or undefined in the call to component.register and then have it so that puts the component in self binding mode. So when the data-bind="component" attribute is encountered for that element it automatically get's it's template from that element.
Then when you do a global applyBindings like "ko.applyBindings({}, document.getElementsByTagName('body')[0];" it would process all the components.
I actually think knockout would be cooler if it required a component based process and ditched "ApplyBindings" entirely.
Just have it process all the component bindings where the binding is the view model for that component and data-binds only work within component contexts.
I think what would be really cool is if you could use the component binding on an existing element and have that elements html be the template for that instance of the component.
You mean this?
https://knockoutjs.com/documentation/component-custom-elements.html#passing-markup-into-components
I think what would be really cool is if you could use the component binding on an existing element and have that elements html be the template for that instance of the component. In which case the component binding wouldn't clone or inject the template it would just bind itself to that element and it's children.
I suppose one would have to write this custom binding, right? By the documentation, the second thing in the component cycle is to discard any content. But I'm not sure if this life cycle behavior is carried on to custom bindings. There's this package that inits the view model with data that was already present - is not used for components, though.
Using @chrisknoll knowledge, I was able to write a really simple function to register components using the html that's in them, it's a neat hack, I like it!!
function registerComponent(pName, pModel, ptemplate) {
ko.components.register(pName, {
viewModel: {
createViewModel: function (params, componentInfo) {
//do some special config stuff here or pass parameters to your view model how you want
var instance = new pModel(params, componentInfo);
return instance;
}
}, template: ptemplate === null ? '<!-- ko template: { nodes: $componentTemplateNodes, data: $data } --><!-- /ko -->' : ptemplate
});
}
So the basic way it works is it calls register component like normal, you pass this a name for your component, and the model it should create for the component. But for the template, you can pass null. If you pass null then the template it uses will be the template syntax for inserting itself into itself using it's own data context as the data context for the template.
<!-- ko template: { nodes: $componentTemplateNodes, data: $data } --><!-- /ko -->
When the component loads your html will be removed, but then immediately reinserted as bound data.
Then you just need to call
ko.applyBindings({});
After all your components are registered.
Here's my whole test index.html I used to tinker on this:
````
<script>
function registerComponent(pName, pModel, ptemplate) {
ko.components.register(pName, {
viewModel: {
createViewModel: function (params, componentInfo) {
//do some special config stuff here or pass parameters to your view model how you want
var instance = new pModel(params, componentInfo);
return instance;
}
}, template: ptemplate === null ? '<!-- ko template: { nodes: $componentTemplateNodes, data: $data } --><!-- /ko -->' : ptemplate
});
}
var loginFormModel = function(params, componentInfo) {
var email = ko.observable('[email protected]');
this.email = email;
var password = ko.observable();
this.password = password;
this.submitClick = function() {
alert('You clicked me.');
}
return this;
}
registerComponent('loginForm', loginFormModel, null);
ko.applyBindings({}, document.getElementsByTagName('body')[0]);
</script>
````
@ryios Nice! Just one question: What would happen to values inside this rendered HTML? Would they be bound to the view model or discarded?
I'm guessing discarded. The HTML will appear there for the first render, but then after apply bindings is applied, it will parse out all the component tags take the content within as template nodes, and re-render.
I think it's the same phenomena as any server side rendering toolchain: html on serverside comes down as raw HTML, and client side library needs to hook up the bindings (such as event handlers, etc).
You can create a new binding handler to init the model from the value attribute, as such:
ko.bindingHandlers.intFromValueAttr = {
init: function (element, valueAccessor) {
valueAccessor()(element.value);
}
};
You might want to implement it better than that, it's kind of dumb (no error checking etc) that way.
Here's a complete example of it working:
````
<script>
function registerComponent(pName, pModel, ptemplate) {
ko.components.register(pName, {
viewModel: {
createViewModel: function (params, componentInfo) {
//do some special config stuff here or pass parameters to your view model how you want
var instance = new pModel(params, componentInfo);
return instance;
}
}, template: ptemplate === null ? '<!-- ko template: { nodes: $componentTemplateNodes, data: $data } --><!-- /ko -->' : ptemplate
});
}
ko.bindingHandlers.intFromValueAttr = {
init: function (element, valueAccessor) {
valueAccessor()(element.value);
}
};
var loginFormModel = function (params, componentInfo) {
var email = ko.observable();
this.email = email;
var password = ko.observable();
this.password = password;
this.submitClick = function () {
alert('You clicked me.');
}
return this;
}
registerComponent('loginForm', loginFormModel, null);
ko.applyBindings({}, document.getElementsByTagName('body')[0]);
</script>
````
@ryios Thanks a lot for all the contribution!
Even though we strayed a bit towards the component usage solution (which was very insightful btw);
I think we have reached the same conclusion that auto-biding view models would not be possible without manual effort since it appears that Knockout does not have a built-in solution (which is curious because components are kind of auto-bound after your register them).
If there are no more contributions, I'd like to know if I can close this issue so we can move one on this subject 馃槂 .
@ryios @chrisknoll @avickers
I ended up tinkering with this solution to bootstrap the knockout application (inspired by the aforementioned StimulusJs). It should allow me to use components and standard viewModels on the page by appending a ko-model
in any element. I'd love some feedback from you guys.
My webpack entry point:
const application = new Application();
// This allows me to register components as specifications and reuse viewModels if I want to
application.registerComponent("click-counter", ClickCounterComponent, ClickCounterViewModel);
application.registerViewModel("resource", ResourceViewModel);
application.start();
ViewModel class:
export default abstract class ViewModel {
private static bound: Array<ViewModel> = [];
private static directive: Symbol = Symbol.for("ko-model");
private name: string;
private allowMultiBind: boolean;
constructor(name: string, allowMultiBind: boolean = false) {
this.name = name;
this.allowMultiBind = allowMultiBind;
}
public bind(): boolean {
let element = $(`[${ViewModel.directive.toString()}=${this.name}]`)[0];
if (element == null || this.isBound(this) || !this.allowMultiBind) return;
else ko.applyBindings(this, element);
ViewModel.bound.push(this);
}
private isBound(viewModel: ViewModel): boolean {
return ViewModel.bound.includes(viewModel);
}
}
Component class:
export default abstract class Component {
constructor(public readonly name: string,
public readonly viewModel: ViewModelConstructor,
public readonly template: string) {
}
public register(): void {
if (ko.components.isRegistered(this.name)) return;
ko.components.register(this.name, {
template: this.template,
viewModel: { createViewModel: this.getInstance }
})
}
private getInstance(params: any, componentInfo: any): ViewModel {
return new this.viewModel(params);
}
}
Application class
export default class Application {
private page: Page;
private viewModels: Array<ViewModelDefinition> = [];
private components: Array<ComponentDefinition> = [];
public async start() {
$(document).ready(() => {
this.bootstrapViewModels();
this.bootstrapComponents();
});
}
public registerViewModel(identifier: string, viewModel: ViewModelConstructor, allowMany: boolean = false) {
let definition = { identifier, constructor: viewModel, allowMany };
if (this.viewModels.includes(definition)) return;
this.viewModels.push(definition);
}
public registerComponent(name: string, component: ComponentConstructor, viewModel: ViewModelConstructor) {
let definition = { constructor: component, name, viewModel };
if (this.components.includes(definition)) return;
this.components.push(definition);
}
private bootstrapComponents(): void {
this.components.forEach(definition => {
let component = new definition.constructor(definition.name, definition.viewModel);
component.register();
});
}
private bootstrapViewModels(): void {
this.page = new Page(window.location.href, document.body, this.viewModels);
this.page.discoverViewModels();
this.page.bindContexts();
}
}
Page class:
export default class Page {
//Keeps url for caching purposes to avoid rebinding viewmodels not used in a page
//Can be usefull to integrate with unpoly
private readonly url: string;
private readonly body: HTMLElement;
private readonly definitions: Array<ViewModelDefinition>;
private readonly contexts: Array<Context> = [];
public constructor(url: string, body: HTMLElement, definitions: Array<ViewModelDefinition>) {
this.url = url;
this.body = body;
this.definitions = definitions;
}
public discoverViewModels(): void {
$("[ko-model]", this.body).each((_, e) => {
let name = $(e).attr("ko-model");
this.contexts.push(new Context(name, e, this.getViewModel(name)));
});
}
public bindContexts(): void {
this.contexts.forEach(context => context.bindViewModel());
}
private getViewModel(name: string): ViewModel {
let definition = this.definitions.find(x => x.identifier == name);
return new definition.constructor(definition.identifier);
}
}
Component example:
// [...]
import template from './index.html';
import './index.scss';
export class ClickCounterComponent extends Component {
constructor(name: string, viewModel: ViewModelConstructor) {
super(name, viewModel, template)
}
}
export class ClickCounterViewModel extends ViewModel {
public count: ko.Observable<number> = ko.observable<number>(0);
constructor(private readonly params: any) {
super(undefined, null);
}
public increment(): void {
this.count(this.count() + 1);
}
}
Wasn't the purpose to be able to support existing html in a non spa like fashion? I'm not seeing where you are using the <!-- ko template: { nodes: $componentTemplateNodes, data: $data } --><!-- /ko -->
trick or another way of using $componentTemplateNodes.
@ryios Actually no, this is where the conversation went after you mentioned the usage of components and I questioned about using it with already rendered HTML 馃槃.
However, this would change the approach to use only components whether I'd like to retain the ability to use viewModels for parts of the page.
The whole point was to find a way to automatically bind the view models to the page like Stimulus does with data-controllers
or Angular does with ng-model
and still being able to use components.
After researching a little bit more, I'm inclining towards having an ApplicationViewModel = {}
bound to the whole page, and attaching other viewModels found in the page using some custom binding like:
<div data-bind="model: dashboardViewModel"></div>
. (I still have to test this)
The previous bootstrap code was meant to replace what I can't do with Webpack, which is exposing modules to the browser and calling <script>ko.applyBindings()</script>
for each page (As I said before, I took a little inspiration from the StimulusJs source code) - That's the whole reason I want to be able to automatically bind view models to the DOM.
PS.: If you already tried doing something like this before, let me know.
PS虏.: I forgot to ask: Could you apply proper code highlighting to the previous snippets you posted? I think there's valuable information there and it will improve readability- if it's not much trouble.
PS鲁 (Update).: As I'm thinking more about this question, wouldn't ko.applyBindings()
to the whole body and a custom binding like I described be sufficient to bind specific view models to elements and still being able to use components?
Why is it that using multiple entries with Webpack wouldn't allow you to expose the correct view models to the correct pages?
You don't even need a custom binding, you can just use the existing "with" binding and you'd have a child/parent hierarchy all the way up your models with $parent and $parents.
You just need a way of getting all your models into the master model, and you could have something like this:
````
var masterModel = function() {
this.loginModel = //=require('loginModel.js');
return this;
}
ko.applyBindings(new masterModel(), bodyElementHere);
And to change contexts to a sub model
````
I used a gulp-include require comment because I don't really know webpack that well and I'm not really doing anything in es6 or typescript so can't add a lot of input there.
Not to drag this issue out, I just find this interesting and I love talking about anything knockout and love learning more about it and other ways other people are doing things.
@avickers By default Webpack does not expose modules in the bundle to the browser. Although, from what I've learned, there are ways to do that by using the output.library
and output.libraryTarget
config, which demands extra work and does not blend very well for my use case. I've opened an Issue on SO sometime ago, to get help on emulating the behavior I had with Brunch using Webpack.
After that, I gave up pursuing the Webpack configuration route and this actually opened my mind to the possibility of not having to manually bind the viewModels on the script tag for each page. So, I borrowed inspiration from other JS frameworks that have this kind of bootstrap mechanism and I'm trying to reproduce it here.
@ryios But In my case, I won't have the view models already attached to the "master view model" on the page load. I want to be able to discover them and load on demand because I can have a number of different combinations for this depending on the page (each page will have different partials views).
So I was making sense that perhaps I could do something like this:
// [...] My bootstrap code [...]
var applicationViewModel = { /* Could have some top-level info about the app to be shared */ };
// On application start (page load)...
// It seems to me that doing this I'll bind the context to the root element;
// all components will be available to use anywhere in the document
// and then I can selectively bind view models to specific parts of this page
ko.applyBindings(applicationViewModel);
And then:
<!-- This custom binder would allow me to search for this partialViewModel
registered in my application, instantiate it, and attach it to the applicationViewModel
while it would still be applied to the context of this particular div -->
<div data-bind="model: partialViewModel"></div>
Not to drag this issue out, I just find this interesting and I love talking about anything knockout and love learning more about it and other ways other people are doing things.
No problem at all, I'm sure I'm the one learning the most from all of this 馃槃.
Ah loading them on demand etc, gotcha.
The way i'm doing things right now, I have a build for 1 output and everything becomes 1 js file, even the CSS. So while I can develop my models and bindings etc in separate files, they all become "xyz.thing.js" with all their styles included.
I load 1 file 1 time instead of having a plethora of files that get loaded on demand and I just throw the whole thing in the head and have footer logic kick it off.
I do split up vendor stuff though
But ideally an output for a project might look like
vendor.min.css.map
app.js
Only vendor.min.js and app.min.js are loaded.
They get quite large, vendor might be 350kb and an SPA I just did (app.min.js) is 185kb. But that 185kb includes all the styles, and a few images compiled to base64 data-url syntax too.
Then we have cache busting setup on those two files we can trigger when needed when we push changes.
All the models are loaded right out the gate, nothing needs to be loaded after that.
It's the whole (1 file, vs 100 files) debate.
Works great for SPA's, I'd probably change my approach if I couldn't do spa's.
It also works great for sub app based perspectives.
I.e. we have a HUGE legacy web app on ASP.Net web forms, and on one specific page, we've got an entire SPA that loads there as part of the legacy web app, it uses the same vendor css etc, but the whole page is interactive, that loads ass one JS file on that specific page.
So short story, we've got a traditional server side HTML web app, with page based SPA's running in it.
I.e. Registration and admission is an SPA, and our entire search experience is an SPA (think like home depot or amazon store) etc.
Really it's a mess.... But the SPA components run great. the legacy app is crap... lol
To handle any kind of loading times where a user would need to wait to interact with something, we use pseudo element techniques (not sure what to call it) but it's like when you go on facebook and it's loading comments, you see animated blank lines of texting fading in and out to indicate it's loading, we've got those all over the place. The whole app does it on page load for about .2 seconds and then it's ready and usable. Searching for something will reveal placeholder search results fading in and out while the real results load and fade in, etc etc etc.
It's snappy, really fast.
Oh forgot to mention, the html is also in my JS file. I use an html-to-js plugin in gulp to compile all the html to javascript strings in a template object and I use knockouts "html" binding to bind them to templates. On components I can just refer to the html string directly by template name. That's only relevant for an spa though.
@ryios I'm actually doing exactly what you described for my setup. The only problem so far is finding the right approach to integrate Knockout 馃槄. I guess I'm in the right direction with the previous example, what do you think about it? Would you do something similar to that? (It would reduce drastically the amount of code necessary to bootstrap the application)
PS.: I'm really not into SPAs, I just prefer Multi-Page Apps and less JS overall (Of course, I'm not building applications that require that much of interactivity, but just enough to make it shine).
I think what you're doing is more or less ok, you should be able to ditch the dependency on document.ready though.
Food for thought, but if your HTML files are something like PHP, or .Net MVC RAZOR files (cshtml) etc and are in the same project as your node.js stuff for the front end assets....
Have you considered running your html through an html preprocessor, like say "gulp-cheerio"?
With cheerio, you can do server side processing on elements in your html and transform them, so you could actually look for your special "ng-tag" and get the model name etc...
Then have some kind of standard placeholder at the bottom of the page where you insert dynamic js to do your apply bindings from a gulp task (or however that would translate to a webpack workflow).
Then it's happening during your build and not at run time and you're not having to do it manually all the time and aren't taking any kind of runtime hit.
However, you'd either have the build manipulating your src html, or you'd have to output the src html to like "wwwroot" (dotnet core) or a dist folder and set w/e backend you are using to serve them from there. I.e. ASP.Net 5 you can create view engines to serve views from outside of default locations.
If you are integrating with legacy apps or separate source hives etc etc then I'd say runtime is the way to do it.
I think what you're doing is more or less ok, you should be able to ditch the dependency on document.ready though.
The only thing preventing me to drop document.ready
right now is that I load my js in the head and not at the end of the page. This comes in hand because I have some CDN code that I load inside partial views (forms) and when you render a partial view with Razor it gets called after the layout was rendered, so if I have a dependency on something like jQuery I wouldn't be able to do it.
Have you considered running your html through an html preprocessor, like say "gulp-cheerio"?
I think this would be nice to consider if I eventually hit a bottleneck in the load time...
For now, I think that I'm not going to have that much to load on the page that would justify such an optimal pipeline. But this was actually a good hint that I'm gonna consider, thanks.
Btw, since I'm gonna be using Knockout alongside with Unpoly, I'm probably be avoiding a lot of unnecessary page loads too.
Well, unfortunately, your philosophy is almost the exact opposite of mine (100% CSR, SPA, aggressive code-spliting), so I'm not sure that I can be of much help. 馃槃
It does seem like a very large share of the KO community is using .Net, so this could be great.
I'd love to know what sort of performance profile you end up experiencing with this approach.
For anybody that wants to see my final solution, I've created a public gist.
Right now, I can easily use components and standard view models side-by-side without any problems.
I can pass parameters when necessary and everything just works. I'm also attaching all view models in the page to a master view model so I can share information if needed.
This a testament to how great this lib is, just the fact that it does not make any assumptions on your general workflow and allows customization like that is awesome (IMHO, something much more difficult to achieve in opinionated frameworks like Angular, Vue and React).
Just a late update on this Issue:
This approach is proving itself so much useful to enhance my traditional MPA applications that I've created a package called knockout.bootstrapper (you can pull it from NPM).
Thanks again to everyone that contributed to this discussion 馃槃馃檹.
Most helpful comment
Just a late update on this Issue:
This approach is proving itself so much useful to enhance my traditional MPA applications that I've created a package called knockout.bootstrapper (you can pull it from NPM).
Thanks again to everyone that contributed to this discussion 馃槃馃檹.