Dynamically composing a UI using Knockout.js - javascript

I'm working with the awesome Knockout.js library on a project and am looking for a way to compose sections of my UI at run-time.
For example I have have a couple of templates (simplified, below) that are made up of child templates. Id like to pass a view model to these and render them, and then be able to append (and remove) the contents from criteria form.
<!-- used with LineGraphModel -->
<script type="text/html" name="linegraph-template">
<div id="LineGraph">
<div data-bind="contextTemplate: { name: 'series-template', data: seriesChoices, context: { selected: series } }"></div>
<div data-bind="contextTemplate: { name: 'xaxis-template', data: xAxisChoices, context: { selected: xaxis } }"></div>
<div data-bind="contextTemplate: { name: 'daterange-template', data: dateRangeChoices, context: { selected: dateRange } }"></div>
<div data-bind="template: { name: 'button-template', data: $data }"></div>
</div>
</script>
<!-- used with PieChartModel -->
<script type="text/html" name="piechart-template">
<div id="PieGraph">
<div data-bind="contextTemplate: { name: 'series-template', data: seriesChoices, context: { selected: series } }"></div>
<div data-bind="contextTemplate: { name: 'daterange-template', data: dateRangeChoices, context: { selected: dateRange } }"></div>
<div data-bind="template: { name: 'button-template', data: $data }"></div>
</div>
</script>
I've begin wandering down the path of ko.renderTemplate but I can't seem to find any good documentation on how to create a new div and append the result to an existing div. Is this possible, or is there another approach I should be trying?

After writing all this down, it dawns on me that this might exceed the scope of your question quite a bit. If that is indeed the case, I apologize; I hope that you still might get some value out of it.
This stuff here comes from a real app I have been working on for several months now. It's a quick and dirty extraction and might contain bugs or typos where I removed app-specific code or simplified it to make it easier to follow.
With it, I can
arbitrarily nest viewmodels
dynamically add viewmodels on the fly
render Knockout templates bound to these nested viewmodels, and use the results flexibly
Here's a quick overview how it works.
Pretend for a second you are going to build an app that shows a list of messages. The user can click on a message to open a modal dialog and reply. We have three viewmodels:
a root viewmodel called Main
a MessageList that takes care of displaying the list of messages
a third one called MessageReply that is responsible for the reply functionality.
All our viewmodel constructors are neatly namespaced in app.viewmodels. Let's set them up:
$(document).ready(function() {
var mainVm,
messageListVm,
messageReplyVm;
// we start with Main as the root viewmodel
mainVm = new app.viewmodels.Main();
// MessageList is a child of Main
messageListVm = mainVm.addChildVm('MessageList');
// and MessageReply in turn is a child of MessageList
messageReplyVm = messageListVm.addChildVm('MessageReply');
// the root is the only one that gets bound directly
ko.applyBindings(mainVm);
});
Our markup looks something like this:
<body>
<!-- context here: the Main viewmodel -->
<div data-bind="childVm: 'MessageList'">
<!-- context here: the MessageList viewmodel -->
<ul data-bind="foreach: messages">
<!-- context here: the individual message object -->
<li>
<p data-bind="text: body, modal: {viewmodelName: 'MessageReply', parentViewmodel: $parent, setupViewmodelWith: $data, templateName: 'message-reply-template'}">
</p>
</li>
</ul>
</div>
</body>
<script id="message-reply-template" type="text/html">
<!-- context here: the MessageReply viewmodel -->
<div>
<textarea data-bind="value: message().body"></textarea>
<input type="submit" data-bind="click: submit">
</div>
</script>
There are two custom bindings in there, childVm and modal. The former just looks up a child viewmodel ands sets it as the binding context, whereas the modal binding is responsible for rendering the template in the correct context and handing the result to a separate JS library.
Viewmodels gain the ability to nest by borrowing constructor functions, a Parent, a Child or both at the same time. Here is the source for them.
Parents
If a viewmodel should be able to have child viewmodels, it borrows the Parent constructor:
app.viewmodels.Main = function Main() {
app.viewmodels.Parent.apply(this);
this.currentUser = //.. imagine the current user being loaded here from somewhere
};
As a parent viewmodel, Main has gained three things:
.addChildVm(string): add a child viewmodel by passing its name. It's automatically looked up in the app.viewmodel namespace.
.getVm(name): returns the child viewmodel named 'name'
._childVms: an observable list containing all the children
Children
Every viewmodel apart from the root Main is at least a child viewmodel. MessageList is both a child to Main, and a parent to MessageReply. Very appropriately to its name, it houses the messages to be displayed in the list.
app.viewmodels.MessageList = function MessageList() {
app.viewmodels.Parent.apply(this);
app.viewmodels.Child.apply(this);
// children need to set this, so we can find them by name through .getVm()
this._viewmodelName = function() { return "MessageList"; };
this.currentUser = null;
this.messages = ko.observableArray([]);
this.init = function init() {
that.currentUser = that._parentVm.currentUser;
var messages = GetMessages() // pseudocode - load our messages from somewhere
this.messages( messages);
};
};
As a child viewmodel, MessageList gains:
the ability to access its parent through this._parentVm
an optional init function, which is called automatically by the parent if present
So above when we added MessageList to Main with
messageListVm = mainVm.addChildVm('MessageList');
, Main
created a new instance of MessageList
added the instance to its own children
and called the childs init
The child then set itself up by getting a reference to the current user, which is mainted by the parent Main viewmodel.
Our last viewmodel: the MessageReply
MessageReply is just a child viewmodel; like it's parent MessageList did itself, it too copies the current user when initialized. It expects to be handed a Message object from the modal binding, then creates a new Message in reply to it. That reply can be edited and submitted through the form in the modal.
app.viewmodels.MessageReply = function MessageReply() {
app.viewmodels.Child.apply(this);
this._viewmodelName = function() { return "MessageReply"; };
var that = this;
this.currentUser = null;
// called automatically by the parent MessageList
this.init = function init() {
that.currentUser = that._parentVm.currentUser;
};
this.messageWeAreReplyingTo = ko.observable();
// our reply
this.message = ko.observable();
// called by the 'modal' binding
this.setup = function setup(messageWeAreReplyingTo) {
// the modal binding gives us the message the user clicked on
this.messageWeAreReplyingTo( messageWeAreReplyingTo );
// imagine that Message is a model object defined somewhere else
var ourReply = new Message({
sender: that.currentUser,
recipient: that.messageWeAreReplyingTo().sender();
});
this.message( ourReply );
};
// this is triggered by the form submit button in the overlay
this.submit = function submit() {
// send the message to the server
}
};
The 'childVm' binding
Source code
<body>
<!-- context here: the Main viewmodel -->
<div data-bind="childVm: 'MessageList'">
<!-- context here: the MessageList viewmodel -->
</div>
This is merely a convenience wrapper around Knockouts own 'with:' binding. It takes a viewmodel name as its value accessor, looks up a child viewmodel of that name in the current binding context, and uses the 'with:' binding to set that child as the new context.
The 'waitForVm' binding
Source code
This isn't used in the example above, but is quite useful if you want to add viewmodels dynamically at runtime, as opposed to before ko.applyBindings. This way, you can delay initializing parts of your application until the user actually wants to interact with them.
waitForVm waits until the specified viewmodel is available before binding its child elements. It does not modify the binding context.
<div data-bind="waitForVm: 'MessageList'">
<!-- bindings in here are not executed until 'MessageList' is loaded -->
<div data-bind="childVm: 'MessageList'"> ... </div>
</div>
The 'modal' binding
Source code
This takes a Knockout template, marries it to a viewmodel, renders it and passes the result to an external JS library that handles the modal dialog.
Imagine that this modal library
when initialized, creates a DOM container before </body>
when asked to display the modal, takes this container and shows it overlayed over the rest of the page, lightbox-style
Let's look at the modal binding in action again:
<!-- context here: the individual message object -->
<li>
<p data-bind="text: body, modal: {viewmodelName: 'MessageReply', parentViewmodel: $parent, setupViewmodelWith: $data, templateName: 'message-reply-template'}">
</p>
</li>
modal will
use the parent viewmodel MessageList, found in our current binding context at $parent
ask it via getVm() for its child viewmodel instance MessageReply
add a click binding to the <p>, which when activated
calls setup() on MessageReply, handing it our $data - the current message the user clicked on
prepares the modal and
renders the template 'message-reply-template', bound to the MessageReply viewmodel, into the modals DOM container

Related

How to pass variables from ng-template declared in parent component to a child component/directive?

So I want to know if there is a way to pass an ng-template and generate all it's content to include variables used in interpolation?
Also I'm still new to angular so besides removing the html element do I need to worry about removing anything else?
At the end of this there will be a link to a stackblitz.com repo which will have all the code shown below.
the following is my src/app/app.component.html code implementing my directive:
<hello name="{{ name }}"></hello>
<p>
Start editing to see some magic happen :)
</p>
<!-- popup/popup.directive.ts contains the code i used in button tag -->
<button PopupDir="" body="this is a hardcoded message that is passed to popup box"> simple
</button>
<ng-template #Complicated="">
<div style="background-color: red;">
a little more complicated but simple and still doable
</div>
</ng-template>
<button PopupDir="" [body]="Complicated">
complicated
</button>
<ng-template #EvenMoreComplicated="">
<!-- name and data isn't being passed i need help here-->
<div style="background-color: green; min-height: 100px; min-width:100px;">
{{name}} {{data}}
</div>
</ng-template>
<button PopupDir="" [body]="EvenMoreComplicated">
more complicated
</button>
the following is my src/app/popup/popup.directive.ts
import { Directive, Input, TemplateRef, ViewContainerRef, HostListener } from '#angular/core'
#Directive({
selector: 'PopupDir, [PopupDir]'
})
export class Popup {
#Input() body: string | TemplateRef<any>;
viewContainer: ViewContainerRef;
popupElement: HTMLElement;
//i dont know if i need this
constructor (viewContainer: ViewContainerRef) {
this.viewContainer = viewContainer;
}
//adds onlick rule to parent tag
#HostListener('click')
onclick () {
this.openPopup();
}
openPopup() {
//Pcreate pupup html programatically
this.popupElement = this.createPopup();
//insert it in the dom
const lastChild = document.body.lastElementChild;
lastChild.insertAdjacentElement('afterend', this.popupElement);
}
createPopup(): HTMLElement {
const popup = document.createElement('div');
popup.classList.add('popupbox');
//if you click anywhere on popup it will close/remove itself
popup.addEventListener('click', (e: Event) => this.removePopup());
//if statement to determine what type of "body" it is
if (typeof this.body === 'string')
{
popup.innerText = this.body;
} else if (typeof this.body === 'object')
{
const appendElementToPopup = (element: any) => popup.appendChild(element);
//this is where i get stuck on how to include the context and then display the context/data that is passed by interpolation in ng-template
this.body.createEmbeddedView(this.viewContainer._view.context).rootNodes.forEach(appendElementToPopup);
}
return popup;
}
removePopup() {
this.popupElement.remove();
}
}
this is the link to the repo displaying my problem:
https://stackblitz.com/edit/popupproblem
First let's think how we're passing context to embedded view. You wrote:
this.body.createEmbeddedView(this.viewContainer._view.context)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Your Popup component is hosted in AppComponent view so this.viewContainer._view.context will be AppComponent instance. But what I want you to tell:
1) Embedded view has already access to scope of the template where ng-template is defined.
2) If we pass context then it should be used only through template reference variables.
this.body.createEmbeddedView(this.viewContainer._view.context)
||
\/
this.body.createEmbeddedView({
name = 'Angular';
data = 'this should be passed too'
})
||
\/
<ng-template #EvenMoreComplicated let-name="name" let-data="data">
{{name}} {{data}}
So in this case you do not need to pass context because it is already there.
this.body.createEmbeddedView({})
||
\/
<ng-template #EvenMoreComplicated>
{{name}} {{data}}
Why UI is not updating?
Angular change detection mechanism relies on tree of views.
AppComponent_View
/ \
ChildComponent_View EmbeddedView
|
SubChildComponent_View
We see that there are two kind of views: component view and embedded view. TemplateRef(ng-template) represents embedded view.
When Angular wants to update UI it simply goes through that view two check bindings.
Now let's remind how we can create embedded view through low level API:
TemplateRef.createEmbeddedView
ViewContainerRef.createEmbeddedView
The main difference between them is that the former simply creates EmbeddedView while the latter creates EmbeddedView and also adds it to Angular change detection tree. This way embedded view becames part of change detection tree and we can see updated bindings.
It's time to see your code:
this.body.createEmbeddedView(this.viewContainer._view.context).rootNodes.forEach(appendElementToPopup);
It should be clear that you're using the first approach. That means you have to take care of the change detection yourself: either call viewRef.detectChanges() manually or attach to tree.
Simple solution could be:
const view = this.body.createEmbeddedView({});
view.detectChanges();
view.rootNodes.forEach(appendElementToPopup);
Stackblitz Example
But it will detect changes only once. We could call detectChanges method on each Popup.ngDoCheck() hook but there is an easier way that is used by Angular itself.
const view = this.viewContainer.createEmbeddedView(this.body);
view.rootNodes.forEach(appendElementToPopup);
We used the second approach of creating embedded view so that template will be automatically checked by Angular itself.
I'm still new to angular so besides removing the html element do I
need to worry about removing anything else?
I think we should also destroy embedded view when closing popup.
removePopup() {
this.viewContainer.clear();
...
}
Final Stackblitz Example

meteor.js: How to access value of HTML-element

Using the meteor.js framework, how can the value of a HTML element be selected in the "meteor way"? By using a jQuery-selector the browser would iterate through the DOM for every item, which is very expensive, wouldn't it?
The meteor tutorial uses a submit form and handles the template variable in a onSubmit-event. But how is it done if there is no onSubmit (and therefore no template-variable containing the element in question?
Could someone help out with the following example given, please?
cars.html
<template name="Car">
<div class="car-item" contenteditable="true">BMW</div>
<div class="edit-bar">save</div>
</template>
cars.js
'click .save'(event, template){
//access content of '.car-item' here when '.save' is clicked
}
You can use the template instance's jQuery. It will scope only the elements of the current template:
The template instance serves as the document root for the selector.
Only elements inside the template and its sub-templates can match
parts of the selector.
This results in a higher performance but requires you to control the granularity and scope of the elements to be searched.
Example of selector scopes
Just compare the output of the follwing example:
'click .save'(event, templateInstance){
//access content of '.car-item' here when '.save' is clicked
// global scope search
console.log($('div'));
// template scope search
console.log(templateInstance.$('div'));
}
Applied to your code
it results in the following code:
'click .save'(event, templateInstance){
// access content of '.car-item' here when '.save' is clicked
const carItems = templateInstance.$('.car-item');
// ... process car items
}
Try adding names to your divs
<div class="car-item" contenteditable="true" name="car">
Then in your click event:
click.save(event, template){
var car = event.target.car.value;
}
Let us know if it worked.

creating new bindingContext to be used by children view models

I have 2 custom controls parent-control and child-control. I need child to execute functionalities on behalf of the parent. and the requirement is that child should be used within parent boundaries.
usage example
...<content-around> <!-- this is 'outerContext' bindingContext -->
<parent-control shared.bind="outerContext.something">
<div>
<child-control property="nameOfMemberOfShared"></child-control>
</div>
<div>
<span>some text here</span>
<child-control property="anotherNameOfMemberOfShared"></child-control>
</div>
</parent-control>
</content-around>...
parent-control.html
<template>
<slot></slot>
</template>
parent-control.ts (assuming all the imports)
export class ParentControlCustomElement {
#bindable shared: any;
bind(bindingContext, overrideContext) {
//here want to make sure elements rendered inside slot
//are able to access 'shared'
}
}
child-control.html
<template>
<!-- this is for simplicity more advanced stuff needed here -->
<h1>${sharedObject[property]}</h1>
</template>
child-control.ts (assuming all imports)
export class ChildControlCustomElement {
#bindable property: string;
sharedObject: any;
bind(bindingContext, overrideContext) {
this.sharedObject = bindingContext.shared;
// the problem is HERE!
// instead of getting a binding context pointing
// to parent-control view model I get binding context
// pointing to 'outerContext'
}
}
How can I make sure that starting at parent-control inner components will get a binding context pointing to parent-control's view model?
If you know your child control will always be used within the parent control you can declare a dependency on the ancestor/parent:
#inject(ParentControlCustomElement)
export class ChildControlCustomElement {
constructor(parentControl) {
this.parentControl = parentControl;
}
}
If it you don't know for sure whether the child control will be used within the parent, use #inject(Optional.of(ParentControlCustomElement)).

Using controller as syntax only updates one of the scopes

I'm a bit confused when it comes to the controller as syntax as I've never worked with it before. I'd like to know the correct way to fix this. Haven't been able to find a similar problem when searching.
I have a menu and a button which toggles the menu. The menu has its own scope and the button has another, as they live in two separate files and containers.
When I click the button it only updates the nav.isActive within the button scope. I created a service for storing the state, which I shouldn't have to do when I think about it.. Should I? Because the only way to watch if that value changes is with a watcher, which would require me to use $scope since this doesn't have the $watch function, however, I'd like to avoid this as much as I can as it will affect performance which is important in this project.
What is the correct way of updating a "scope" variable from another "scope" when using the controller as syntax?
Controller:
nav.controller('NavCtrl', ['navState', function(navState) {
var nav = this;
nav.menu = [
{icon: 'color_lens', href: ''},
{icon: 'color_lens', href: 'about'},
{icon: 'color_lens', href: 'contact'},
{icon: 'color_lens', href: 'logout'},
{icon: 'color_lens', href: 'faq'}
];
nav.toggleMenu = function() {
nav.isActive = navState.checkState();
}
}]);
The service for passing the value from one scope to the other:
nav.service('navState', [function() {
var isActive = false;
this.checkState = function() {
isActive = !isActive;
return !isActive;
}
}]);
The menu markup (menu.html):
<nav class="si-wrapper si-dark" ng-controller="NavCtrl as nav" ng-class="{active: nav.isActive}">
<ul class="si-list">
<li class="si-item" ng-repeat="item in nav.menu">
<a href="#/{{item.href}}">
<div class="si-inner-item">
<i class="material-icons md-36" ng-bind="item.icon"></i>
</div>
</a>
</li>
</ul>
<h1>{{nav.isActive}}</h1> <!-- This doesn't change -->
</nav>
The button which toggles the menu (header.html):
<div ng-controller="NavCtrl as nav">
<button ng-click="nav.toggleMenu()">Toggle</button>
<span>{{nav.isActive}}</span> <!-- This updates however -->
</div>
Your problem is that nav.isActive is not set for one of the controllers. Basically whenever you use ng-controller a new controller (and $scope) is created. So for each of your controllers, $scope.isActive needs to be set for the related view to reference it.
In your posted code, isActive is only set when toggleMenu() is run, which happens only in header.html.
In order to get your code to work, simply set isActive on controller load. For example:
nav.controller('NavCtrl', ['navState', function(navState) {
// put isActive on the scope on controller load
this.activeState = navState.getState();
nav.toggleMenu = function() {
navState.toggleState();
};
}]);
You need to make your service better by separating state access and state manipulation. Also wrapping it in a container object will ensure you won't have any scope hierarchy issues.
nav.service('navState', [function() {
var state = {
isActive: false
};
this.toggleState = function() {
state.isActive = !state.isActive;
};
this.getState = function(){
return state;
};
}]);
Then you need to use `activeState.isActive' inside your view.
Now you will have a service with a shareable state, and two controllers that both reference the same service on load. Then inside your menu view, when you toggle the state, both controllers scopes are updated.
From the angularjs docs:
When a Controller is attached to the DOM via the ng-controller directive, Angular will instantiate a new Controller object, using the specified Controller's constructor function. A new child scope will be created and made available as an injectable parameter to the Controller's constructor function as $scope.
Additionally, the ng-controller directive has a scope that inherits from its parent scope. So, you can also define the shared data in the parent. It will be accessible on the 'NavCtrl' scope. But since you want to use the controller as synax one way would be to share the data with a help of a service.
However, I don't like the approach with the 'ng-controller' directive. The other solution is to replace it with a hierarchy of directives where the child directives will require the parent one. Also use 'controllerAs' property there.

Meteor to not rerender when certain fields change

Meteor re renders a view when a document changes.
Template.story.data = function() {
var storyID = Session.get('storyID');
var story = Stories.findOne({
_id: storyID
})
if (!story)
return;
return story;
};
Here's a template helper for the story template, getting a story from the Stories document.
When fields like story.title changes I want the template to rerender. But when fields like story.viewingusers change I don't want to rerender the template. Anyway to make that happen?
The specific problem that triggered the question was solved by setting the publish function to not publish those fields. However that solution doesn't work for every usecase and a general solution is needed.
What you're looking for is the #constant template helper.
What i'd do in your case is wrap the popover markup in a constant block, then update the content in that block manually in the story.rendered function.
So something like this:
story.html:
<template name="story">
...
{{#constant}}
<!-- Popover markup here -->
{{/constant}}
...
</template>
client.js:
Template.story.rendered = function(){
//Update logic here
}

Categories