HotTowel: Viewmodel lose mapping when navigating between pages - knockout-mapping-plugin

I just started with HotTowel and very impressed. I manage to learn how to use it with knockout binding.
Here's the problem I'm facing:
I have a website with 3 pages: Home, About, Jar
In Jar I have:
define(['services/logger', 'repo/jarrepo'], function (logger, repo) {
var Jar = function () {
this.Id = ko.observable(0);
this.Name = ko.observable('');
}
this.jars = ko.mapping.fromJS([]);
var vm = {
title: ko.observable('JARS'),
jars: jars,
AddJar: function () {
this.jars.push(new Jar);
},
DeleteJar: function (jar) {
jars.destroy(jar);
},
activate: function () {
repo.getJars(this.jars);
return true;
}
};
return vm;
});
And in the view I have:
<div data-bind="foreach: jars">
<label data-bind="text: name" />
Delete
</div>
My repo code:
$.getJSON("/api/jar/getjars", function (data) {
ko.mapping.fromJS(data, jars);
});
It works fine if I'm on Jar page. However, when I navigate to Home and come back, the ko mapping is lost. I check it by using chrome console and check for the the jars array and all of its elements has lost the ko.mapping node in the attribute. Thus I cannot delete the jar but can still add new one to it.
Can anyone help me figure out how to remap the object after navigation?
Thanks.

Your repo.getJars(this.jars); is an asyncronous task, so you must return a promise in your activate function to things work properly.
I Think that this will do the trick:
activate: function () {
return repo.getJars(this.jars);
}
Your repo should also return the promise:
function getJars(jars){
return $.getJson(...);
}

Related

Using Fragment to insert HTML rendered on the back end via dangerouslySetInnerHTML

I used to compile and insert JSX components via
<div key={ ID } dangerouslySetInnerHTML={ { __html: HTML } } />
which wrapped my HTML into a <div>:
<div>my html from the HTML object</div>
Now react > 16.2.0 has support for Fragments and I wonder if I can use that somehow to avoid wrapping my HTML in a <div> each time I get data from the back end.
Running
<Fragment key={ ID } dangerouslySetInnerHTML={ { __html: HTML } } />
will throw a warning
Warning: Invalid prop `dangerouslySetInnerHTML` supplied to `React.Fragment`. React.Fragment can only have `key` and `children` props.
in React.Fragment
Is this supported yet at all? Is there another way to solve this?
Update
Created an issue in the react repo for it if you want to upvote it.
Short Answer
Not possible:
key is the only attribute that can be passed to Fragment. In the
future, we may add support for additional attributes, such as event
handlers.
https://reactjs.org/docs/fragments.html
You may want to chime in and suggest this as a future addition.
https://github.com/facebook/react/issues
In the Meantime
You may want to consider using an HTML parsing library like:
https://github.com/remarkablemark/html-react-parser
Check out this example to see how it will accomplish your goal:
http://remarkablemark.org/blog/2016/10/07/dangerously-set-innerhtml-alternative/
In Short
You'll be able to do this:
<>
{require('html-react-parser')(
'<em>foo</em>'
)}
</>
Update December 2020
This issue (also mentioned by OP) was closed on Oct 2, 2019. - However, stemming from the original issue, it seems a RawHTML component has entered the RFC process but has not reached production, and has no set timeline for when a working solution may be available.
That being said, I would now like to allude to a solution I currently use to get around this issue.
In my case, dangerouslySetInnerHTML was utilized to render plain HTML for a user to download; it was not ideal to have additional wrapper tags included in the output.
After reading around the web and StackOverflow, it seemed most solutions mentioned using an external library like html-react-parser.
For this use-case, html-react-parser would not suffice because it converts HTML strings to React element(s). Meaning, it would strip all HTML that wasn't standard JSX.
Solution:
The code below is the no library solution I opted to use:
//HTML that will be set using dangerouslySetInnerHTML
const html = `<div>This is a div</div>`
The wrapper div within the RawHtml component is purposely named "unwanteddiv".
//Component that will return our dangerouslySetInnerHTML
//Note that we are using "unwanteddiv" as a wrapper
const RawHtml = () => {
return (
<unwanteddiv key={[]}
dangerouslySetInnerHTML={{
__html: html,
}}
/>
);
};
For the purpose of this example, we will use renderToStaticMarkup.
const staticHtml = ReactDomServer.renderToStaticMarkup(
<RawHtml/>
);
The ParseStaticHtml function is where the magic happens, here you will see why we named the wrapper div "unwanteddiv".
//The ParseStaticHtml function will check the staticHtml
//If the staticHtml type is 'string'
//We will remove "<unwanteddiv/>" leaving us with only the desired output
const ParseStaticHtml = (html) => {
if (typeof html === 'string') {
return html.replace(/<unwanteddiv>/g, '').replace(/<\/unwanteddiv>/g, '');
} else {
return html;
}
};
Now, if we pass the staticHtml through the ParseStaticHtml function you will see the desired output without the additional wrapper div:
console.log(ParseStaticHtml(staticHtml));
Additionally, I have created a codesandbox example that shows this in action.
Notice, the console log will throw a warning: "The tag <unwanteddiv> is unrecognized in this browser..." - However, this is fine because we intentionally gave it a unique name so we can easily differentiate and target the wrapper with our replace method and essentially remove it before output.
Besides, receiving a mild scolding from a code linter is not as bad as adding more dependencies for something that should be more simply implemented.
i found a workaround
by using react's ref
import React, { FC, useEffect, useRef } from 'react'
interface RawHtmlProps {
html: string
}
const RawHtml: FC<RawHtmlProps> = ({ html }) => {
const ref = useRef<HTMLDivElement>(null)
useEffect(() => {
if (!ref.current) return
// make a js fragment element
const fragment = document.createDocumentFragment()
// move every child from our div to new fragment
while (ref.current.childNodes[0]) {
fragment.appendChild(ref.current.childNodes[0])
}
// and after all replace the div with fragment
ref.current.replaceWith(fragment)
}, [ref])
return <div ref={ref} dangerouslySetInnerHTML={{ __html: html }}></div>
}
export { RawHtml }
Here's a solution that works for <td> elements only:
type DangerousHtml = {__html:string}
function isHtml(x: any): x is DangerousHtml {
if(!x) return false;
if(typeof x !== 'object') return false;
const keys = Object.keys(x)
if(keys.length !== 1) return false;
return keys[0] === '__html'
}
const DangerousTD = forwardRef<HTMLTableCellElement,Override<React.ComponentPropsWithoutRef<'td'>,{children: ReactNode|DangerousHtml}>>(({children,...props}, ref) => {
if(isHtml(children)) {
return <td dangerouslySetInnerHTML={children} {...props} ref={ref}/>
}
return <td {...props} ref={ref}>{children}</td>
})
With a bit of work you can make this more generic, but that should give the general idea.
Usage:
<DangerousTD>{{__html: "<span>foo</span>"}}</DangerousTD>

Unable to load Meteor template, set context and bind events

I am trying to render and append a template (ticket_edit) to the body. I need to set a context to the newly appended template, and the events of ticket_edit should be bound to that template.
The code:
Template.ticket.events = {
'click a.edit' : function (event) {
//when the edit button has been clicked, load template 'ticket_edit'
//with the current context. Please see situation 1 and 2.
}
}
Template.ticket_edit.events = {
'click a.save' : function (event) {
//this won't do anything when i have supplied a context!
}
}
So the problem is:
-I can set the context, but then the events are not bound to the newly added template.
-If I don't set the context the events are bound properly.
But i need both the events and the context.
Situation 1:
'click a.edit' : function (event) {
//applying a context to the template will result in events not being bound.
$('body').append(Meteor.render(Template.ticket_edit(this)));
}
Sitation 2:
'click a.edit' : function (event) {
//this way, the events will execute properly and i can save my ticket.
//However, no context is supplied!
$('body').append(Meteor.render(Template.ticket_edit));
}
Does anybody have a good method for doing this? I'm fairly new to Meteor, so maybe you have a better method of dynamically loading templates.
Don't use jQuery for this, just do it directly with the template and a #with block. Something like this:
<body>
{{> tickets}}
</body>
<template name="tickets">
{{#each tickets}}
{{> ticket}}
{{/each}}
{{#with currentTicket}}
{{> editTicket}}
{{/with}}
</template>
Template.tickets.tickets = function() {
return Tickets.find();
};
Template.tickets.currentTicket = function () {
return Tickets.findOne({ _id: Session.get( "currentTicket" ) });
};
Template.ticket.events({
'click a.edit' : function () {
Session.set( "currentTicket", this._id );
}
});
Because we're using a #with block, the editTicket template won't get rendered until you click the edit button (setting the "currentTicket" in the Session).
It's also possible to just do this (no #with block):
{{> editTicket currentTicket}}
But that causes editTicket to always be rendered, just without any context until the Session var gets set.
Note that because we're using Session, the user won't be interrupted by reloads/hot code pushes.

Does Knockout.mapping make ALL nested objects observable?

I am trying to map all possible nested objects of a JSON object so that each and every one is becomes an observable. I was under the impression that the use of ko.mapping.fromJS would result in all objects and their objects becoming observable. However, I am not seeing that happen.
If you look at the JSFiddle and code below you will see that the span initially displays the value "Test". My intention is for the button click to update the viewModel with the contents of stuff2, which should change the span's value to "Test2". However, the button click does not update anything.
http://jsfiddle.net/Eves/L5sgW/38/
HTML:
<p> <span>Name:</span>
<span data-bind="text: IntroData.Name"></span>
<button id="update" data-bind="click: Update">Update!</button>
</p>
JS:
var ViewModel = function (data) {
var me = this;
ko.mapping.fromJS(data, {}, me);
me.Update = function () {
ko.mapping.fromJS(stuff2, {}, windows.viewModel);
};
return me;
};
var stuff = {
IntroData: {
Name: 'Test'
}
};
var stuff2 = {
IntroData: {
Name: 'Test2'
}
};
window.viewModel = ko.mapping.fromJS(new ViewModel(stuff));
ko.applyBindings(window.viewModel);
Is it just that I have to make use of mapping options to have the nested objects be made observable? If so, what if the JSON object is so vast and complex (this one obviously isn't)? Can some recursive functionality be used to loop through each object's nested objects to make them all observable?
Modifying the Update function as below will work.
me.Update = function () {
ko.mapping.fromJS(stuff2, {}, windows.viewModel);
};

How to put a delay on AngularJS instant search?

I have a performance issue that I can't seem to address. I have an instant search but it's somewhat laggy, since it starts searching on each keyup().
JS:
var App = angular.module('App', []);
App.controller('DisplayController', function($scope, $http) {
$http.get('data.json').then(function(result){
$scope.entries = result.data;
});
});
HTML:
<input id="searchText" type="search" placeholder="live search..." ng-model="searchText" />
<div class="entry" ng-repeat="entry in entries | filter:searchText">
<span>{{entry.content}}</span>
</div>
The JSON data isn't even that large, 300KB only, I think what I need to accomplish is to put a delay of ~1 sec on the search to wait for the user to finish typing, instead of performing the action on each keystroke. AngularJS does this internally, and after reading docs and other topics on here I couldn't find a specific answer.
I would appreciate any pointers on how I can delay the instant search.
UPDATE
Now it's easier than ever (Angular 1.3), just add a debounce option on the model.
<input type="text" ng-model="searchStr" ng-model-options="{debounce: 1000}">
Updated plunker:
http://plnkr.co/edit/4V13gK
Documentation on ngModelOptions:
https://docs.angularjs.org/api/ng/directive/ngModelOptions
Old method:
Here's another method with no dependencies beyond angular itself.
You need set a timeout and compare your current string with the past version, if both are the same then it performs the search.
$scope.$watch('searchStr', function (tmpStr)
{
if (!tmpStr || tmpStr.length == 0)
return 0;
$timeout(function() {
// if searchStr is still the same..
// go ahead and retrieve the data
if (tmpStr === $scope.searchStr)
{
$http.get('//echo.jsontest.com/res/'+ tmpStr).success(function(data) {
// update the textarea
$scope.responseData = data.res;
});
}
}, 1000);
});
and this goes into your view:
<input type="text" data-ng-model="searchStr">
<textarea> {{responseData}} </textarea>
The mandatory plunker:
http://plnkr.co/dAPmwf
(See answer below for a Angular 1.3 solution.)
The issue here is that the search will execute every time the model changes, which is every keyup action on an input.
There would be cleaner ways to do this, but probably the easiest way would be to switch the binding so that you have a $scope property defined inside your Controller on which your filter operates. That way you can control how frequently that $scope variable is updated. Something like this:
JS:
var App = angular.module('App', []);
App.controller('DisplayController', function($scope, $http, $timeout) {
$http.get('data.json').then(function(result){
$scope.entries = result.data;
});
// This is what you will bind the filter to
$scope.filterText = '';
// Instantiate these variables outside the watch
var tempFilterText = '',
filterTextTimeout;
$scope.$watch('searchText', function (val) {
if (filterTextTimeout) $timeout.cancel(filterTextTimeout);
tempFilterText = val;
filterTextTimeout = $timeout(function() {
$scope.filterText = tempFilterText;
}, 250); // delay 250 ms
})
});
HTML:
<input id="searchText" type="search" placeholder="live search..." ng-model="searchText" />
<div class="entry" ng-repeat="entry in entries | filter:filterText">
<span>{{entry.content}}</span>
</div>
In Angular 1.3 I would do this:
HTML:
<input ng-model="msg" ng-model-options="{debounce: 1000}">
Controller:
$scope.$watch('variableName', function(nVal, oVal) {
if (nVal !== oVal) {
myDebouncedFunction();
}
});
Basically you're telling angular to run myDebouncedFunction(), when the the msg scope variable changes. The attribute ng-model-options="{debounce: 1000}" makes sure that msg can only update once a second.
<input type="text"
ng-model ="criteria.searchtext""
ng-model-options="{debounce: {'default': 1000, 'blur': 0}}"
class="form-control"
placeholder="Search" >
Now we can set ng-model-options debounce with time and when blur, model need to be changed immediately otherwise on save it will have older value if delay is not completed.
For those who uses keyup/keydown in the HTML markup.
This doesn't uses watch.
JS
app.controller('SearchCtrl', function ($scope, $http, $timeout) {
var promise = '';
$scope.search = function() {
if(promise){
$timeout.cancel(promise);
}
promise = $timeout(function() {
//ajax call goes here..
},2000);
};
});
HTML
<input type="search" autocomplete="off" ng-model="keywords" ng-keyup="search()" placeholder="Search...">
Debounced / throttled model updates for angularjs : http://jsfiddle.net/lgersman/vPsGb/3/
In your case there is nothing more to do than using the directive in the jsfiddle code like this:
<input
id="searchText"
type="search"
placeholder="live search..."
ng-model="searchText"
ng-ampere-debounce
/>
Its basically a small piece of code consisting of a single angular directive named "ng-ampere-debounce" utilizing http://benalman.com/projects/jquery-throttle-debounce-plugin/ which can be attached to any dom element. The directive reorders the attached event handlers so that it can control when to throttle events.
You can use it for throttling/debouncing
* model angular updates
* angular event handler ng-[event]
* jquery event handlers
Have a look : http://jsfiddle.net/lgersman/vPsGb/3/
The directive will be part of the Orangevolt Ampere framework (https://github.com/lgersman/jquery.orangevolt-ampere).
Just for users redirected here:
As introduced in Angular 1.3 you can use ng-model-options attribute:
<input
id="searchText"
type="search"
placeholder="live search..."
ng-model="searchText"
ng-model-options="{ debounce: 250 }"
/>
I believe that the best way to solve this problem is by using Ben Alman's plugin jQuery throttle / debounce. In my opinion there is no need to delay the events of every single field in your form.
Just wrap your $scope.$watch handling function in $.debounce like this:
$scope.$watch("searchText", $.debounce(1000, function() {
console.log($scope.searchText);
}), true);
Another solution is to add a delay functionality to model update. The simple directive seems to do a trick:
app.directive('delayedModel', function() {
return {
scope: {
model: '=delayedModel'
},
link: function(scope, element, attrs) {
element.val(scope.model);
scope.$watch('model', function(newVal, oldVal) {
if (newVal !== oldVal) {
element.val(scope.model);
}
});
var timeout;
element.on('keyup paste search', function() {
clearTimeout(timeout);
timeout = setTimeout(function() {
scope.model = element[0].value;
element.val(scope.model);
scope.$apply();
}, attrs.delay || 500);
});
}
};
});
Usage:
<input delayed-model="searchText" data-delay="500" id="searchText" type="search" placeholder="live search..." />
So you just use delayed-model in place of ng-model and define desired data-delay.
Demo: http://plnkr.co/edit/OmB4C3jtUD2Wjq5kzTSU?p=preview
I solved this problem with a directive that basicly what it does is to bind the real ng-model on a special attribute which I watch in the directive, then using a debounce service I update my directive attribute, so the user watch on the variable that he bind to debounce-model instead of ng-model.
.directive('debounceDelay', function ($compile, $debounce) {
return {
replace: false,
scope: {
debounceModel: '='
},
link: function (scope, element, attr) {
var delay= attr.debounceDelay;
var applyFunc = function () {
scope.debounceModel = scope.model;
}
scope.model = scope.debounceModel;
scope.$watch('model', function(){
$debounce(applyFunc, delay);
});
attr.$set('ngModel', 'model');
element.removeAttr('debounce-delay'); // so the next $compile won't run it again!
$compile(element)(scope);
}
};
});
Usage:
<input type="text" debounce-delay="1000" debounce-model="search"></input>
And in the controller :
$scope.search = "";
$scope.$watch('search', function (newVal, oldVal) {
if(newVal === oldVal){
return;
}else{ //do something meaningful }
Demo in jsfiddle: http://jsfiddle.net/6K7Kd/37/
the $debounce service can be found here: http://jsfiddle.net/Warspawn/6K7Kd/
Inspired by eventuallyBind directive http://jsfiddle.net/fctZH/12/
Angular 1.3 will have ng-model-options debounce, but until then, you have to use a timer like Josue Ibarra said. However, in his code he launches a timer on every key press. Also, he is using setTimeout, when in Angular one has to use $timeout or use $apply at the end of setTimeout.
Why does everyone wants to use watch? You could also use a function:
var tempArticleSearchTerm;
$scope.lookupArticle = function (val) {
tempArticleSearchTerm = val;
$timeout(function () {
if (val == tempArticleSearchTerm) {
//function you want to execute after 250ms, if the value as changed
}
}, 250);
};
I think the easiest way here is to preload the json or load it once on$dirty and then the filter search will take care of the rest. This'll save you the extra http calls and its much faster with preloaded data. Memory will hurt, but its worth it.

When to call YUI destroy?

When should destroy be called? Does it ever get called automatically by YUI lifecycle? Does the page unload cause the YUI lifecycle to call destroy on all objects created during the page processing? I have been working under the assumption that I need to make all my own calls to destroy but that gets hairy when ajax calls replace sections of code that I had progressively enhanced. For example:
<div id="replaceMe">
<table>
<tr>
<td>1</td>
</tr>
<tr>
<td>2</td>
</tr>
</table>
<script>
YUI().use('my-lib', function(Y) {
Y.mypackage.enhanceTable("replaceMe");
});
<script>
</div>
The my-lib module basically adds a click handler and mouseover for each row:
YUI.add('my-lib', function(Y) {
function EnhancedTable(config) {
EnhancedTable.superclass.constructor.apply(this, arguments);
}
EnhancedTable.NAME = "enhanced-table";
EnhancedTable.ATTRS = {
containerId : {},
onClickHandler : {},
onMouseoverHandler : {},
onMouseoutHandler : {}
};
Y.extend(EnhancedTable, Y.Base, {
_click : function(e) {
//... submit action
},
destructor : function() {
var onClickHandler = this.get("onClickHandler"),
onMouseoverHandler = this.get("onMouseoverHandler"),
onMouseoutHandler = this.get("onMouseoutHandler");
onClickHandler && onClickHandler.detach();
onMouseoverHandler && onMouseoverHandler.detach();
onMouseoutHandler && onMouseoutHandler.detach();
},
initializer : function(config) {
var container = Y.one("[id=" + this.get("containerId") + "]");
this.set("container", container);
this.set("onMouseoverHandler", container.delegate("mouseover",
this._mouseover, "tr", this ));
this.set("onMouseoutHandler", container.delegate("mouseout",
this._mouseout, "tr", this ));
this.set("onClickHandler", container.delegate("click",
this._click, "tr", this ));
},
_mouseout : function(e) {
e.currentTarget.removeClass("indicated");
},
_mouseover : function(e) {
e.currentTarget.addClass("indicated");
}
});
Y.namespace("mypackage");
Y.mypackage.enhanceTable = function(containerId) {
var enhancedTable new EnhancedTable({containerId:containerId});
};
}, '0.0.1', {
requires : [ 'base', 'node' ]
});
The click handler would submit a request back to my application that would change the page. Do I need to remember all the enhancedTable objects and have an onunload handler call the destroy method of each? Or does the YUI framework take care of this?
The last part of this quesiton is, I also have code outside of this that replaces the whole table by replacing the content of the <div id="replaceMe">. In doing so, the script would get re-run and augment the new <table> with a new EnhancedTable. Do I need to remember the old table, and destroy it before the new table clobbers it?
Instead of setting handlers as attributes I'd store them all in an array like this:
this._handlers = [
container.delegate("mouseover", this._mouseover, "tr", this ),
container.delegate("mouseout", this._mouseout, "tr", this ),
container.delegate("click", this._click, "tr", this )
];
Then add a destructor method that does the following
destructor : function() {
new Y.EventTarget(this._handlers).detach();
}
It accomplishes the same thing but with way less work on your part!
Ideally instead of running this against each table you'd attach all your delegates to #replaceMe so that it wouldn't need to be recreated each time you changed the content, no matter where that happened from.
YUI won't automatically call .destroy() for you on unload, it will clean up DOM subs though. The above is extra credit that's really only necessary if you are going to be destroying the object yourself.

Resources