Using html as input for a knockout-component - javascript

I am in the process of implementing some components in my codebase. However, I have ran into an smaller issue with the template part. I would like to send in the template as an input to a knockout-component but I am not sure how to do it or if it even is possible.
Taking an example from http://knockoutjs.com/documentation/component-overview.html I hope that I can do something like this:
<like-or-dislike params="value: userRating">
<div class="like-or-dislike" data-bind="visible: !chosenValue()">
<button data-bind="click: like">Like it</button>
<button data-bind="click: dislike">Dislike it</button>
</div>
<div class="result" data-bind="visible: chosenValue">
You <strong data-bind="text: chosenValue"></strong> it.
And this was loaded from an external file.
</div>
</like-or-dislike>
But I cannot find any documentation if that works at all. The reason why I want to implement it that way is simply because I am having some server generated html that I want to still be a part of a component. Otherwise I will have to make it a json-object and render the html inside the component which seems like a unnecessary extra step. The good thing about using components is that the logic is seperated in it's own file and it is easier to seperate logic between different components. I understand that if I do it like this I have to copy the html if I want to reuse the component.
Am I thinking of this the wrong way or is this possible?
Thanks for your sage advice and better wisdom.

I can't say I fully understand your situation but I think I may have the answer. You can actually have the server generate <script type="text/html"> and use that (by id of course) with a component. The KO documentation is pretty poor on component templating, but here is an example using an element.
A couple of things I've learned with components. The viewmodel must be declared before declaration, and the <script> must be in the dom prior to binding.
function ComponentViewModel() {
var self = this;
self.Title = ko.observable("This is a Component VM");
}
function ViewModel() {
var self = this;
self.ExampleComponent = ko.observable({
name: 'Example'
});
}
ko.components.register('Example', {
template: {
element: 'ComponentTemplate'
},
viewModel: ComponentViewModel
})
ko.applyBindings(new ViewModel());
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<script id="ComponentTemplate" type="text/html">
<span data-bind="text: Title"></span>
</script>
<div data-bind="component: ExampleComponent"> </div>
I won't devalue components, but I also would point you to using templates with a data binding, it's essentially the same thing (please correct me if I'm wrong). and doesn't require the component be established. This is better for situations where the would-be component occurs less frequently.
function ComponentViewModel() {
var self = this;
self.Title = ko.observable("This is a Template with a VM");
}
function ViewModel() {
var self = this;
self.ComponentVM = ko.observable(new ComponentViewModel());
self.ExampleComponent = ko.observable({
name: 'ExampleTemplate', // This is the ID
data: self.ComponentVM
});
}
ko.applyBindings(new ViewModel());
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<script id="ExampleTemplate" type="text/html">
<span data-bind="text: Title"></span>
</script>
<div data-bind="template: ExampleComponent"> </div>
I hope these help!

Related

Clean separation of view and code with Vue using templates

I hope this is a trivial problem for someone and I am just missing a tiny magic piece of code: I am migrating from knockout.js to vue.js and I am wondering if there is a simple way to use templates the same way in vue.js as in knockout.js. The following code snipped uses knockout.js and works as expected:
function viewmodel() {
this.person = ko.observable(new person());
}
function person() {
this.first = ko.observable('hello');
this.last = ko.observable('world');
}
ko.applyBindings(new viewmodel());
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script type="text/html" id="person-template">
<p data-bind="text: first"/>
<p data-bind="text: last"/>
</script>
<div>
<div data-bind="template: { name: 'person-template', data: person }"/>
</div>
However, I can't figure out how to achieve the same in vue.js. I am aware of components in vue.js and all that, but I want to keep the structure as it is and not put more view-specific code into my JavaScript files. (I am already not happy with the el: '#app', but that's my least concern at the moment) Of course this code doesn't work:
var app = new Vue({
el: '#app',
data: {
person: {
first: 'hello',
last: 'world'
}
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.3.3/vue.js"></script>
<script type="text/x-template" id="person-template">
<p v-text="first"/>
<p v-text="last"/>
</script>
<div id="app">
<div><!-- how? is this even possible? --></div>
</div>
Is there a way to get this working by only changing the HTML part of code? Preferably only the <div>-Element.
So this is not quite only changing the template, but a single additional property can work here. Specify a template in the Vue.
var app = new Vue({
el: '#app',
template: "#person-template",
data: {
person: {
first: 'hello',
last: 'world'
}
}
});
I also modified your template slightly because a template for any Vue or component requires one root element. I recommend you do not use self closing tags in Vue as I've seen this cause issues. Vue requires valid HTML5 and self closing tags are not valid HTML5.
<script type="text/x-template" id="person-template">
<div>
<p v-text="person.first"></p>
<p v-text="person.last"></p>
</div>
</script>
Working example.
Additionally, a very common way of specifying templates in this fashion, without using the kind of hacky script tag is to use the HTML5 template element.
<template id="person-template">
<div>
<p v-text="person.first"></p>
<p v-text="person.last"></p>
</div>
</template>
Finally, knowing you are slightly unhappy with the el:"#app" You can leave the el property out of the Vue definition and mount the Vue on an element of your choice using the $mount method. So if you wanted to go completely hog wild on separation of concerns you could do something like this.
const renderer = Vue.compile(document.querySelector("#person-template").innerHTML)
const example = {
data: {
person: {
first: 'hello',
last: 'world'
}
}
}
const app = new Vue(Object.assign({}, example, renderer))
app.$mount("#app")
Example.
I should mention nobody really does that (compiling their templates manually, it's common to use $mount) that I'm aware of. It also depends on using the version of Vue that includes the compiler (which you might not have if you are using webpack or some other build tool for example). You could also do something like this:
const app = new Vue(Object.assign({}, example, {template: "#person-template"}))
app.$mount("#app")
which would basically allow you to specify a template, a component, and a place to mount it all separately.

How does childscope work on an ng-if statement? Specifically in a <SELECT> element

NOTE: I'm a new member here so I couldn't directly comment and ask for clarification.
So, my question is: How can I work around ng-if creating a child scope for a select element?
I have the following code:
HTML
<select id="project-select"
ng-if="projects.length > 0"
ng-options="project.name for project in projects"
ng-model="currentProject"
ng-change="broadcastChange('project-changed', currentProject)">
</select>
And my controller is set up in the following format:
function Controller() {
//Do code stuffz
}
angular
.module('app')
.controller('Controller', Controller);
I learned from this post that the "ng-if" is creating a child scope.
So even when the model changes, this part stays the same because it is a primitive value: (name is just a string)
<div id="current-project" class="pull-left">
<strong>Project: </strong>{{currentProject.name}}
</div>
Furthermore in the aforementioned post there were a couple options.
a. Simply change to this: ng-model="$parent.currentProject" which feels a little hacky
b. Set the object value in the controller, which I'm not entirely sure how to do. I feel like it's an easy fix, but I'm somehow overcomplicating it.
Anyway, for now I simply changed ng-if to ng-show and that's solved the problem. However, I am trying to understand Angular more deeply and I feel like this issue could be explained a little bit better to me. Thanks in advance!
What you will find with Angular scope variables is: always use a dot.
That's the mantra from the excellent ng-book
In your case, what this means is this:
You have this code:
<select id="project-select"
ng-if="projects.length > 0"
ng-options="project.name for project in projects"
ng-model="currentProject"
ng-change="broadcastChange('project-changed', currentProject)">
</select>
Which means that you are binding to a $scope variable called $scope.currentProject.
Because of the mysterious and awesome way that javascript works, this does not get updated when you are inside of a child scope.
Thankfully, the solution is actually quite simple. Instead, create an object like so:
$scope.myData = {
currentProject: ''
}
And in your markup, bind to that like so:
<select id="project-select"
ng-if="projects.length > 0"
ng-options="project.name for project in projects"
ng-model="myData.currentProject"
ng-change="broadcastChange('project-changed', myData.currentProject)">
</select>
And voila. It will update, even though it's in a child scope.
This is actually quite useful, because you now have a way to "meaningfully" group variables together. Here's some other pseudo-code to demonstrate what I mean:
$scope.projectData = {
currentProjectID: 1,
currentProjectTitle: 'My Cool Project',
projects: [
{id: 1, name: 'My Cool Project'},
{id: 2, name: 'Another Project'}
],
someOtherProperty: false
// ...etc....
}
As a side-note, this section of this article might be helpful: http://docstore.mik.ua/orelly/webprog/jscript/ch11_02.htm#jscript4-CHP-11-SECT-2.1
If all you want to do is show/hide the select element based on the projects in your 'Controller' controller scope, then ng-show is the right way to go here. In my experience, I've used ng-if when I'm conditionally loading a larger "partial" view containing numerous controls where I felt a separate scope was necessary to avoid having a very large scope (or to facilitate re-use).
You are correct. Do not use $parent in any production Angular apps. It makes your model dependent on the structure of your view, which makes your code hard to refactor and less modularized.
Binding to object properties is the way to go, as you suggested in your "b" answer. The recommended way to do this in the latest version of Angular 1.x is by using the "controller as" syntax. This method makes use of "prototypical inheritance" in javascript. There is a good explanation on this here: http://javascript.info/tutorial/inheritance
I created a plunker for you to demonstrate how binding to object properties works in nested scopes. take a look at the "controller as" syntax". also, try changing the value of ctrl.testBinding in the input, you will see that reflected in the ng-if child scope. I will try to find some links to explain this in more detail.
https://plnkr.co/edit/Gx5xbkJXgzjPSG8kajPR?p=preview
<!DOCTYPE html>
<html >
<head>
<link rel="stylesheet" href="style.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.5.7/angular.min.js"></script>
<script src="script.js"></script>
</head>
<body ng-app="testApp">
<div ng-controller="testCtrl as ctrl">
<input ng-model="ctrl.testBinding" type="text"/>
<button ng-click="ctrl.toggle()">toggle show</button>
<div ng-if="ctrl.show">
{{ ctrl.testBinding }}
</div>
</div>
</body>
</html>
//script.js
function testController($scope) {
var vm = this;
vm.show = true;
vm.toggle = function(){
vm.show = !vm.show
}
}
angular
.module('testApp', [])
.controller('testCtrl', testController);

What is root admin in knockout.js and what it points to?

data-bind="visible: $root.isAdministrator"
Can someone please explain what the above line means in knockout.js?
$root is a variable that saves the root ViewModel. In this page there is anywhere defined a ViewModel like this:
var vm = {
isAdministrator:ko.observable(true);
aRootObserable:ko.observable("ROOT");
childVm: {
childObservable:ko.observable("CHILD");
}
}
ko.applyBindings(vm);
It is used in knockout.js when the scope is on an underlying ViewModel but you want to access the root ViewModel. Example:
<div data-bind="with: childVm">
<div data-bind="text: childObservable"><!-- result is CHILD --></div>
<div data-bind="text: $root.aRootObserable"><! -- result is ROOT --></div>
</div>
If you are reengineering a knockout webpage you can search in all JS files for applyBindings. The parameter that is passed to this function is the root ViewModel.

Handlebarsjs: render multiple passes with nested templates

I see many things that just refers me to partials which sucks because they have to built out of the layout context.
What I'm wanting to do is make nested templates
For example:
<div id="person">
{{name}}
<div id="address">
{{street}}
</div>
</div>
<script>
var outer = Handlebars.compile($('#person').html());
outer({ name: 'someone special' });
var inner = Handlebars.compile($('#address').html());
inner({ street: 'somewhere cool' });
</script>
When running this, the inner template is never rendered as the outer templating gobbles it up.
It would be nice if you could namespace nested templates like this:
{{> name}}
<div id="person">
{{name}}
{{> address}}
<div id="address">
{{street}}
</div>
{{/> address}}
</div>
{{/> name}}
<script>
var outer = Handlebars.compile($('#person').html(), 'name');
outer({ name: 'someone special' });
var inner = Handlebars.compile($('#address').html(), 'address');
inner({ street: 'somewhere cool' });
</script>
or something like this, so that when the outer renders, it will leave the address alone and let inner render address itself without removing it from the DOM.
Is anything like this possible?
The reason for this question is that I'm using backbone and want to separate out all of my small views but it compiles to one file. when the outer is templated with handlebars, everything else breaks. I don't want to use partials as that just take everything out of the flow of the html document for the designers.
EDIT
I think what I really need is a way to do {{noparse}} and from the registerHelper just return the raw html between the noparse tags
Here is a demo of a no-parse helper. To use this functionality, you will need to use a version of at least v2.0.0-alpha.1. You can get it from the handlebars build page. Here is the pull request that details about it.
Here is the relevant code.
Template
<script id="template" type="text/x-handlebars-template">
<div id="person">
{{name}}
{{{{no-parse}}}}
<div id="address">
{{street}}
</div>
{{{{/no-parse}}}}
</div>
</script>
Handlebars.registerHelper('no-parse', function(options) {
return options.fn();
});
You're missing a crucial step. You have to take the template & compile it (which you're doing) but you also have to register the partial.
Really the pattern you want to use is the partial template - where you register the inner template as a partial template then fill it with the person's address data - there is a great example here, but this also will help in your specific HTML setup.
<script id="person-template" type="text/x-handlebars-template">
{{#each person}}
{{name}}
{{> address}}
{{/each}}
</script>
<script id="address-partial" type="text/x-handlebars-template">
<div class="address">
<h2>{{street}}</h2>
</div>
</script>
<script type="text/javascript">
$(document).ready(function() {
var template = Handlebars.compile($("#person-template").html());
Handlebars.registerPartial("address", $("#address-partial").html());
template({ name: 'someone special', street: 'somewhere cool' });
}
</script>

knockout: accessing the View Model through an iframe?

The app I'm building requires the use of iframes for wysiwyg text editing, and these iframes need to be wired up to the viewModel, but I'm finding that this clashes with knockout... or at least knockout seems not to apply bindings when I try to access them through the parent object.
Here's some code...
<script type="text/javascript">
$(function(){
ko.applyBindings(parent.model.project, $('#root')[0]);
});
</script>
<ul id="root" data-bind="template: {name: function(){return type()},
foreach: populate() }"></ul>
<script id="document" type="text/html">
<li class="draft" draft="${draft()}" data-bind="css: {expanded: $data.expanded}">
<span data-bind="click: function(){parent.model.project.expand($data, 'draft')}">
${ordinal(draft())} Draft
<img src="icons/close-black.png"
data-bind="click: function(){parent.model.project.deleteDraft($data)},
css:{ only: function() {parent.model.project.drafts > 1} }"/>
</span>
<div>
<ul data-bind="css: {expanded: $data.expanded},
template: {
name: 'draft',
foreach: $data.draftItems,
}"
>
</ul>
</div>
</li>
</script>
<script id="draft" type="text/html">
{{if $data.name}}
<li class="${name}">${name}</li>
{{/if}}
</script>
OK, this isn't a wysiwyg text editor, but it still illustrates my point.
Now the thing is, when I wrote this it worked perfectly. I had the part of viewModel that all the bindings refer to defined in a js file accessed only by this html... but I need the same ViewModel to be accessed by the parent window, as I would with a wysiwyg editor for toolbar buttons and other external controls, so I moved that part of the viewModel to the file where the rest of it was defined... and now it doesn't work!
In the external file that I had previously I was still accessing the parent view model using parent.model, but now not having direct exclusive access to that model it doesn't seem to work. The thing is though that I can access the view model with console.log, I can document.write from it too, it fires off events back to the viewModel, and my view updates initially, but after that initial one it no longer updates.
Is there a way to solve this?
iframes won't inherit bindings from parent elements.
You can't make it work that way, as iframes really are separate pages within another page.
Each iframe will need to have its own view model. If that viewmodel needs to come from another view model, you'll need to share that data via global JS objects or message passing or some other mechanism.

Categories

Resources