Nesting options under an if binding causes wrong behavior - html

I want to have a <select> without using the options binding, and nest the <option> element under an if binding.
The following is what I did (here's also a fiddle), which displays a behavior I wasn't expecting: The if seems to fire for each option selection, whereas what I expected is that it would fire only when adding the options elements to DOM.
Thus, when an option is chosen, it doesn't displayed. Only when choosing the same option again, it renders as it should.
What did I do wrong?
var DogHouseViewModel = function() {
var self = this;
self.allowedNames = ["A", "B", "C"];
self.puppies = ko.observableArray([]);
self.createPuppy = function () {
self.puppies.push(new DogViewModel());
}
self.isNameAlreadyTaken = function (puppyName) {
var puppies = self.puppies();
for (var i = 0; i < puppies.length; i++) {
if (puppies[i].dogName() == puppyName) {
return true;
}
}
return false;
}
self.printPuppiesName = function () {
self.puppies().forEach(function (puppy) {
alert(puppy.dogName())
})
}
}
var DogViewModel = function (dogName) {
var self = this;
self.dogName = ko.observable();
}
vm = new DogHouseViewModel()
ko.applyBindings(vm);
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/2.1.0/knockout-min.js"></script>
<div>
<button data-bind="text: 'create a puppy', click: createPuppy"></button>
<button data-bind="text: 'print puppies names', click: printPuppiesName"></button>
<div data-bind="foreach: puppies">
<select data-bind="value: dogName">
<!-- ko foreach: $parent.allowedNames -->
<!-- ko if: !($root.isNameAlreadyTaken($data)) -->
<option data-bind="value: $data, text: $data"></option>
<!-- /ko -->
<!-- /ko -->
</select>
</div>
</div>

I believe this is what you are trying to do. The problem is that your allowedNames are the values of the options do you can't just remove them from the array. But you can clone the parent array and as it changes compute the array by returning a list of not used values.
I also added a check to make sure we don't accidentally add a puppy object to the puppies array when no names are available.
var DogHouseViewModel = function() {
var self = this;
self.allowedNames = ["A", "B", "C"];
self.puppies = ko.observableArray([]);
self.createPuppy = function() {
var newPuppy = new DogViewModel(self);
if(newPuppy.allowedNames().length > 0) { // Check to see if there are any names left.
self.puppies.push(newPuppy);
}
}
self.removePuppy = function(obj) {
self.puppies.remove(obj);
}
self.printPuppiesName = function() {
self.puppies().forEach(function(puppy) {
alert(puppy.dogName())
})
}
}
var DogViewModel = function(parent) {
var self = this;
self.dogName = ko.observable();
self.allowedNames = ko.computed(function() {
var allowedNamesClone = parent.allowedNames.slice(0);
var usedNames = parent.puppies().filter(function(pup) { // get all pups who have a name
return pup.dogName() !== '' && pup.dogName() !== self.dogName();
})
usedNames.forEach(function(pup) {
var index = allowedNamesClone.indexOf(pup.dogName());
if (index > -1) {
allowedNamesClone.splice(index, 1); // remove name from cloned array
}
})
return allowedNamesClone; // return new array
})
}
vm = new DogHouseViewModel()
ko.applyBindings(vm);
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<div>
<button data-bind="text: 'create a puppy', click: createPuppy"></button>
<button data-bind="text: 'print puppies names', click: printPuppiesName"></button>
<div data-bind="foreach: puppies">
<!-- ko if: allowedNames().length > 0 -->
<select data-bind="options: allowedNames, value: dogName"></select>
<button data-bind="click: $root.removePuppy">x</button>
<!-- /ko -->
</div>
</div>

Related

Change the observable to another on the html dynamically

I'm trying to update the observable on the html by changing the visibility, I thought it should update the binding but is not happening, is there another way to update the binding?
// This is a simple *viewmodel* - JavaScript that defines the data and behavior of your UI
function AppViewModel() {
this.isVisible = ko.observable(true);
this.counter0 = ko.observable(0);
this.counter1 = ko.observable(0);
this.counterDisplay = this.counter0;
this.add = function() {
console.log(this.counterDisplay());
const newValue = this.counterDisplay() + 1;
this.counterDisplay(newValue);
};
this.changeCounter = () => {
this.isVisible(!this.isVisible());
if(this.counterDisplay === this.counter0) {
this.counterDisplay = this.counter1;
console.log('change to counter1');
} else {
this.counterDisplay = this.counter0;
console.log('change to counter0');
}
this.isVisible(true);
}
}
// Activates knockout.js
ko.applyBindings(new AppViewModel());
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<p>Counter Main: <div data-bind="text: counterDisplay, visible: isVisible"></div></p>
<button data-bind="click: add">Add</button>
<button data-bind="click: changeCounter">Change</button>
<p>Counter0: <div data-bind="text: counter0"></div></p>
<p>Counter1: <div data-bind="text: counter1"></div></p>
On the example the counter main is displaying the value for counter 0, but after click on Change button the counter main should change to display the counter1 value, I thought that changing the visibility should re-render the DOM and bind to the counter1 value but it remains with the counter0 binding.
Visibility binding does not affect bindings itself, it only changes DOM element display status.
Changing binding can be achieved by using ko.cleanNode(DOMElement) but it should only be used if you really need to rebuild binding completely, which is not the case 99 times of 100.
In your case it is easier to simply create an observable which stores the index of active counter and a computed which displays active counter value. See the code below.
// This is a simple *viewmodel* - JavaScript that defines the data and behavior of your UI
function AppViewModel() {
const self = this;
self.activeCounterIndex = ko.observable('0');
self.counter0 = ko.observable(0);
self.counter1 = ko.observable(0);
this.activeCounterValue = ko.computed(function(){
return self['counter'+self.activeCounterIndex()]();
});
this.add = function() {
const newValue = self['counter'+self.activeCounterIndex()]() + 1;
self['counter'+self.activeCounterIndex()](newValue);
};
this.changeCounter = () => {
if (self.activeCounterIndex() === '0') {
self.activeCounterIndex('1');
} else {
self.activeCounterIndex('0');
}
}
}
// Activates knockout.js
ko.applyBindings(new AppViewModel());
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<p>Active counter (#<span data-bind="text:activeCounterIndex"></span>): <b data-bind="text: activeCounterValue"></b></p>
<button data-bind="click: add">Increment active counter</button>
<button data-bind="click: changeCounter">Swich counter</button>
<p>Counter #0: <span data-bind="text: counter0"></span></p>
<p>Counter #1: <span data-bind="text: counter1"></span></p>

Knockout observable field in array to indicate order of items

I have a html view that's connected to Knockout viewmodel, and displays a list of items.
Each item in the list contains a textual name field, and a numeric order field.
A user can perform a "drag and drop" action to items in the UL list.
The "drag and drop" event changes the order of the items as follows:
<div id="wrapper">
<ul data-bind="foreach:Items">
<li draggable="true"
ondragover="event.preventDefault();"
data-bind="event:{dragstart:$root.dragItem,drop:$root.dropItem}">
<label data-bind="text:name"></label>
<label data-bind="text:orderNo"></label>
<input type="text" data-bind="value:name" />
</li>
</ul>
<script type="text/javascript">
var list = [{ name: 'Red', orderNo: 0 }
, { name: 'Green', orderNo: 1 }
, { name: 'Blue', orderNo: 2 }];
function viewmodel() {
var self = this;
self.Items = ko.mapping.fromJS(list);
self.ItemToDrag = ko.observable();
self.dragItem = function (item, event) {
self.ItemToDrag(item);
return true;
}
self.dropItem = function (item, event) {
event.preventDefault();
var up = self.ItemToDrag().orderNo() > item.orderNo();
self.ItemToDrag().orderNo(up ? item.orderNo() - 0.5 : item.orderNo() + 0.5);
//order this list
self.Items.sort(function (left, right) {
return left.orderNo() == right.orderNo() ? 0 : (left.orderNo() < right.orderNo() ? -1 : 1);
});
//set integer number
for (var i = 0; i < self.Items().length; i++) {
self.Items()[i].orderNo(i);
}
}
}
var vm;
$(document).ready(function () {
vm = new viewmodel();
ko.applyBindings(vm, $("#wrapper")[0]);
});
My question is, if it is possible with Knockout to change the contents of the order field automatically when the items of the list change their order through the UI.
Something like
<ul data-bind="foreach:Items,orderKey:orderNo"></ul>
Where orderKey indicates the order of the items, and which field to update in case of order change.
I'm not sure this is exactly what you need. This is custom binding, that sorts an array from foreach binding before:
ko.bindingHandlers.foreach["after"] = ["orderKey"];
ko.bindingHandlers.orderKey = {
update: function (el, valueAccessor, allBindingsAccessor, viewModel) {
var key = ko.unwrap(valueAccessor());
var allBindings = allBindingsAccessor();
if("foreach" in allBindings) {
var array = ko.unwrap(allBindings.foreach);
array.sort(function(a, b) { return a[key] > b[key]; });
allBindings.foreach = array;
}
}
};
// The model
var model = { Items: ko.observableArray([{text: 3}, {text: 1}, {text: 2}]) };
// Apply
ko.applyBindings(model);
// This simulate changes in observableArray
setTimeout(function() { model.Items.push({text: 0}) }, 1000);
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.0/knockout-min.js"></script>
<ul data-bind="foreach: Items, orderKey: 'text'">
<li data-bind="text: text"></li>
</ul>
No, there is no specific binding for that use case. In knockout, however, it is simple to write a custom binding. See the documentation. In the company I'm working for, we're using a knockout-based framework (developed by us) with tons of custom bindings, some of them really complex.
I just started to create such a binding for your use case. But I realized, it won't fit the purpose unless you have dozens of such lists.
What you can do, however, is to sort put the actual sorting into a knockout computed and just do the updating of the sort index in your drop function. See example below and don't hesitate to ask if something is not clear.
var list = [{ name: 'Red', orderNo: 0 }
, { name: 'Green', orderNo: 1 }
, { name: 'Blue', orderNo: 2 }];
function viewmodel() {
var self = this;
self._items = ko.mapping.fromJS(list);
self.Items = ko.pureComputed(function () {
return self._items().sort(function (a, b) {
return a.orderNo() < b.orderNo() ? -1 : 1;
});
});
self.ItemToDrag = ko.observable();
self.dragItem = function (item, event) {
self.ItemToDrag(item);
return true;
}
self.dropItem = function (item, event) {
event.preventDefault();
var up = self.ItemToDrag().orderNo() > item.orderNo();
self.ItemToDrag().orderNo(up ? item.orderNo() - 0.5 : item.orderNo() + 0.5);
}
}
var vm;
$(document).ready(function () {
vm = new viewmodel();
ko.applyBindings(vm, $("#wrapper")[0]);
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout.mapping/2.4.1/knockout.mapping.min.js"></script>
<div id="wrapper">
<ul data-bind="foreach:Items">
<li draggable="true"
ondragover="event.preventDefault();"
data-bind="event:{dragstart:$root.dragItem,drop:$root.dropItem}">
<label data-bind="text:name"></label>
<label data-bind="text:orderNo"></label>
<input type="text" data-bind="value:name" />
</li>
</ul>

Angular bind-html + $sce still remove the inline style

I write simple custom filter that return string value.
The value contain html string and inline style
When binding angular remove the styles
Here is my filter
siteApp.filter('specialText', ['$filter', '$sce', function ($filter, $sce) {
return function (code, items, defaults) {
var out = defaults;
if (items && items.length) {
var arr = $filter('filter')(items, { code: code }, true);
if (arr && arr.length > 0) {
out = arr[0].value;
$sce.trustAsHtml(out);
}
}
return out;
};
}]);
And This is my html
<div ng-bind-html="'body_message' | specialText : specific_texts :''"></div>
The original text contain inline style but angular remove inline style on binding
How can I keep the inline styles
FULL EXAMPLE CODE
<body >
<div ng-app="siteApp">
<div ng-controller="ctrl">
{{'test1' | specialText : arr :'missing....' }}
<div ng-bind-html="'test1' | specialText : arr :'missing....'"></div>
</div>
</div>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.5.6/angular.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular-sanitize/1.5.6/angular-sanitize.js"></script>
<script>
var siteApp = angular.module('siteApp', ['ngSanitize']);
siteApp.filter('specialText', ['$filter', '$sce', function ($filter, $sce) {
return function (code, items, defaults) {
var out = defaults;
if (items && items.length) {
var arr = $filter('filter')(items, { code: code }, true);
if (arr && arr.length > 0) {
out = arr[0].value;
$sce.trustAsHtml(out);
}
}
return out;
};
}]);
siteApp.controller('ctrl', ['$scope', function ($scope) {
$scope.arr = [{ "code": "test1", "rte": true, "value": "<p style=\"text-align: left;\">First Row</p>\n<h1 style=\"text-align: center;\">Second Center Row</h1>" }];
}]);
</script>
</body>
You are not using the output of $sce.trustAsHtml(out). Inside your if statement try out = $sce.trustAsHtml(out); and it should be ok.

Add search filter inside the select dropdown in AngularJS

I want to add a search filter inside a select dropdown in angularJS.
I have used ng-options to list down the options and used filter to filter out the data in the search box , but the problem is that the search box is not coming inside(or under) select dropdown. (When I click the select dropdown, it shows a search filter and below it has all the options)
Below is the code for your reference :
<div class="rowMargin">
<label class="control-label" for="entitySel">Entity:</label>
<div class="controls">
<select id="entityId" class="input-medium" type="text" name="entityId" ng-model="payment.entityId" ng-options="entityOpt for entityOpt in paymentEntityOptions">
<option value="">Select</option>
</select>
<span ng-show=" submitted && addPayment.entityId.$error.required">
<label class="error">Please provide entity Id </label>
</span>
<div ng-show="payment.entityId == \'Individual\'">
<span>
<select ng-model="payment.entity.individual" ng-options = "individual for individual in individualEntities | filter : filterEntity">
<option value="">Select Individual Entity</option>
<option>
<input type="search" placeholder="Search" ng-model="filterEntity"></input>
</option>
</select>
</span>
</div>
<div ng-show="payment.entityId == \'Group\'">
<span>
<select ng-model="payment.entity.group" ng-options = "group for group in groupEntities | filter : filterEntity">
<option value="">Select Group Entity</option>
<input type="search" placeholder="Search" ng-model="filterEntity"></input>
</select>
</span>
</div>
</div>
I have used the bootstrap button with class 'dropdown-toggle' and on click of the button I have appended an input search box as following :
<div class="dropdown pull-right makePaymentDropdownMainDiv" auto-close="outsideClick">
<button class="btn btn-default dropdown-toggle makePaymentDropdownBtn" type="button" id="individualDrop" data-toggle="dropdown">{{payment.entity}}<span class="caret pull-right"></span></button>
<span ng-show="submitted"><label class="error">Select an Individual</label></span>
<ul class="dropdown-menu makePaymentDropdownUlStyle" role="menu" aria-labelledby="individualDrop">
<input disable-auto-close type="search" ng-model="serchFilter" class="makePaymentDropdownSearchBox" placeholder="Search"></input>
<li role="presentation" ng-repeat="indi in individuals | filter: serchFilter"><a role="menuitem" ng-click="selectEntity(indi)">{{indi}}</a></li>
</ul>
</div>
Showing the 'li' using ng-repeat.
Remember to add auto-close="outsideClick" to your dropdown so that it doesn't close on filtering attempt.
Sorry, I'm rather late to the party, but to me it sounds like you need acute-select, an open source extension (MIT license) to Angular that does exactly this, without further dependencies.
They also have a demo page, which shows what it can do nicely.
you can use easy and best way to search filter inside the select dropdown in AngularJS
Working Demo : http://plnkr.co/edit/o767Mg6fQoyc7jKq77If?p=preview
(function (angular, undefined) {
'use strict';
// TODO: Move to polyfill?
if (!String.prototype.trim) {
String.prototype.trim = function () {
return this.replace(/^\s+|\s+$/g, '');
};
}
/**
* A replacement utility for internationalization very similar to sprintf.
*
* #param replace {mixed} The tokens to replace depends on type
* string: all instances of $0 will be replaced
* array: each instance of $0, $1, $2 etc. will be placed with each array item in corresponding order
* object: all attributes will be iterated through, with :key being replaced with its corresponding value
* #return string
*
* #example: 'Hello :name, how are you :day'.format({ name:'John', day:'Today' })
* #example: 'Records $0 to $1 out of $2 total'.format(['10', '20', '3000'])
* #example: '$0 agrees to all mentions $0 makes in the event that $0 hits a tree while $0 is driving drunk'.format('Bob')
*/
function format(value, replace) {
if (!value) {
return value;
}
var target = value.toString();
if (replace === undefined) {
return target;
}
if (!angular.isArray(replace) && !angular.isObject(replace)) {
return target.split('$0').join(replace);
}
var token = angular.isArray(replace) && '$' || ':';
angular.forEach(replace, function (value, key) {
target = target.split(token + key).join(value);
});
return target;
}
var module = angular.module('AxelSoft', []);
module.value('customSelectDefaults', {
displayText: 'Select...',
emptyListText: 'There are no items to display',
emptySearchResultText: 'No results match "$0"',
addText: 'Add',
searchDelay: 300
});
module.directive('customSelect', ['$parse', '$compile', '$timeout', '$q', 'customSelectDefaults', function ($parse, $compile, $timeout, $q, baseOptions) {
var CS_OPTIONS_REGEXP = /^\s*(.*?)(?:\s+as\s+(.*?))?\s+for\s+(?:([\$\w][\$\w\d]*))\s+in\s+([\s\S]+?)(?:\s+track\s+by\s+([\s\S]+?))?$/;
return {
restrict: 'A',
require: 'ngModel',
link: function (scope, elem, attrs, controller) {
var customSelect = attrs.customSelect;
if (!customSelect) {
throw new Error('Expected custom-select attribute value.');
}
var match = customSelect.match(CS_OPTIONS_REGEXP);
if (!match) {
throw new Error("Expected expression in form of " +
"'_select_ (as _label_)? for _value_ in _collection_[ track by _id_]'" +
" but got '" + customSelect + "'.");
}
elem.addClass('dropdown custom-select');
// Ng-Options break down
var displayFn = $parse(match[2] || match[1]),
valueName = match[3],
valueFn = $parse(match[2] ? match[1] : valueName),
values = match[4],
valuesFn = $parse(values),
track = match[5],
trackByExpr = track ? " track by " + track : "",
dependsOn = attrs.csDependsOn;
var options = getOptions(),
timeoutHandle,
lastSearch = '',
focusedIndex = -1,
matchMap = {};
var itemTemplate = elem.html().trim() || '{{' + (match[2] || match[1]) + '}}',
dropdownTemplate =
'<a class="dropdown-toggle" data-toggle="dropdown" href ng-class="{ disabled: disabled }">' +
'<span>{{displayText}}</span>' +
'<b></b>' +
'</a>' +
'<div class="dropdown-menu">' +
'<div stop-propagation="click" class="custom-select-search">' +
'<input class="' + attrs.selectClass + '" type="text" autocomplete="off" ng-model="searchTerm" />' +
'</div>' +
'<ul role="menu">' +
'<li role="presentation" ng-repeat="' + valueName + ' in matches' + trackByExpr + '">' +
'<a role="menuitem" tabindex="-1" href ng-click="select(' + valueName + ')">' +
itemTemplate +
'</a>' +
'</li>' +
'<li ng-hide="matches.length" class="empty-result" stop-propagation="click">' +
'<em class="muted">' +
'<span ng-hide="searchTerm">{{emptyListText}}</span>' +
'<span class="word-break" ng-show="searchTerm">{{ format(emptySearchResultText, searchTerm) }}</span>' +
'</em>' +
'</li>' +
'</ul>' +
'<div class="custom-select-action">' +
(typeof options.onAdd === "function" ?
'<button type="button" class="btn btn-primary btn-block add-button" ng-click="add()">{{addText}}</button>' : '') +
'</div>' +
'</div>';
// Clear element contents
elem.empty();
// Create dropdown element
var dropdownElement = angular.element(dropdownTemplate),
anchorElement = dropdownElement.eq(0).dropdown(),
inputElement = dropdownElement.eq(1).find(':text'),
ulElement = dropdownElement.eq(1).find('ul');
// Create child scope for input and dropdown
var childScope = scope.$new(true);
configChildScope();
// Click event handler to set initial values and focus when the dropdown is shown
anchorElement.on('click', function (event) {
if (childScope.disabled) {
return;
}
childScope.$apply(function () {
lastSearch = '';
childScope.searchTerm = '';
});
focusedIndex = -1;
inputElement.focus();
// If filter is not async, perform search in case model changed
if (!options.async) {
getMatches('');
}
});
if (dependsOn) {
scope.$watch(dependsOn, function (newVal, oldVal) {
if (newVal !== oldVal) {
childScope.matches = [];
childScope.select(undefined);
}
});
}
// Event handler for key press (when the user types a character while focus is on the anchor element)
anchorElement.on('keypress', function (event) {
if (!(event.altKey || event.ctrlKey)) {
anchorElement.click();
}
});
// Event handler for Esc, Enter, Tab and Down keys on input search
inputElement.on('keydown', function (event) {
if (!/(13|27|40|^9$)/.test(event.keyCode)) return;
event.preventDefault();
event.stopPropagation();
switch (event.keyCode) {
case 27: // Esc
anchorElement.dropdown('toggle');
break;
case 13: // Enter
selectFromInput();
break;
case 40: // Down
focusFirst();
break;
case 9:// Tab
anchorElement.dropdown('toggle');
break;
}
});
// Event handler for Up and Down keys on dropdown menu
ulElement.on('keydown', function (event) {
if (!/(38|40)/.test(event.keyCode)) return;
event.preventDefault();
event.stopPropagation();
var items = ulElement.find('li > a');
if (!items.length) return;
if (event.keyCode == 38) focusedIndex--; // up
if (event.keyCode == 40 && focusedIndex < items.length - 1) focusedIndex++; // down
//if (!~focusedIndex) focusedIndex = 0;
if (focusedIndex >= 0) {
items.eq(focusedIndex)
.focus();
} else {
focusedIndex = -1;
inputElement.focus();
}
});
resetMatches();
// Compile template against child scope
$compile(dropdownElement)(childScope);
elem.append(dropdownElement);
// When model changes outside of the control, update the display text
controller.$render = function () {
setDisplayText();
};
// Watch for changes in the default display text
childScope.$watch(getDisplayText, setDisplayText);
childScope.$watch(function () { return elem.attr('disabled'); }, function (value) {
childScope.disabled = value;
});
childScope.$watch('searchTerm', function (newValue) {
if (timeoutHandle) {
$timeout.cancel(timeoutHandle);
}
var term = (newValue || '').trim();
timeoutHandle = $timeout(function () {
getMatches(term);
},
// If empty string, do not delay
(term && options.searchDelay) || 0);
});
// Support for autofocus
if ('autofocus' in attrs) {
anchorElement.focus();
}
var needsDisplayText;
function setDisplayText() {
var locals = { };
locals[valueName] = controller.$modelValue;
var text = displayFn(scope, locals);
if (text === undefined) {
var map = matchMap[hashKey(controller.$modelValue)];
if (map) {
text = map.label;
}
}
needsDisplayText = !text;
childScope.displayText = text || options.displayText;
}
function getOptions() {
return angular.extend({}, baseOptions, scope.$eval(attrs.customSelectOptions));
}
function getDisplayText() {
options = getOptions();
return options.displayText;
}
function focusFirst() {
var opts = ulElement.find('li > a');
if (opts.length > 0) {
focusedIndex = 0;
opts.eq(0).focus();
}
}
// Selects the first element on the list when the user presses Enter inside the search input
function selectFromInput() {
var opts = ulElement.find('li > a');
if (opts.length > 0) {
var ngRepeatItem = opts.eq(0).scope();
var item = ngRepeatItem[valueName];
childScope.$apply(function () {
childScope.select(item);
});
anchorElement.dropdown('toggle');
}
}
function getMatches(searchTerm) {
var locals = { $searchTerm: searchTerm }
$q.when(valuesFn(scope, locals)).then(function (matches) {
if (!matches) return;
if (searchTerm === inputElement.val().trim()/* && hasFocus*/) {
matchMap = {};
childScope.matches.length = 0;
for (var i = 0; i < matches.length; i++) {
locals[valueName] = matches[i];
var value = valueFn(scope, locals),
label = displayFn(scope, locals);
matchMap[hashKey(value)] = {
value: value,
label: label/*,
model: matches[i]*/
};
childScope.matches.push(matches[i]);
}
//childScope.matches = matches;
}
if (needsDisplayText) setDisplayText();
}, function() {
resetMatches();
});
}
function resetMatches() {
childScope.matches = [];
focusedIndex = -1;
};
function configChildScope() {
childScope.addText = options.addText;
childScope.emptySearchResultText = options.emptySearchResultText;
childScope.emptyListText = options.emptyListText;
childScope.select = function (item) {
var locals = {};
locals[valueName] = item;
var value = valueFn(childScope, locals);
//setDisplayText(displayFn(scope, locals));
childScope.displayText = displayFn(childScope, locals) || options.displayText;
controller.$setViewValue(value);
anchorElement.focus();
typeof options.onSelect === "function" && options.onSelect(item);
};
childScope.add = function () {
$q.when(options.onAdd(), function (item) {
if (!item) return;
var locals = {};
locals[valueName] = item;
var value = valueFn(scope, locals),
label = displayFn(scope, locals);
matchMap[hashKey(value)] = {
value: value,
label: label/*,
model: matches[i]*/
};
childScope.matches.push(item);
childScope.select(item);
});
};
childScope.format = format;
setDisplayText();
}
var current = 0;
function hashKey(obj) {
if (obj === undefined) return 'undefined';
var objType = typeof obj,
key;
if (objType == 'object' && obj !== null) {
if (typeof (key = obj.$$hashKey) == 'function') {
// must invoke on object to keep the right this
key = obj.$$hashKey();
} else if (key === undefined) {
key = obj.$$hashKey = 'cs-' + (current++);
}
} else {
key = obj;
}
return objType + ':' + key;
}
}
};
}]);
module.directive('stopPropagation', function () {
return {
restrict: 'A',
link: function (scope, elem, attrs, ctrl) {
var events = attrs['stopPropagation'];
elem.bind(events, function (event) {
event.stopPropagation();
});
}
};
});
})(angular);
<body ng-app="Demo">
<div class="container" ng-controller="DemoController">
<label>Level 1</label>
<div custom-select="g for g in nestedItemsLevel1 | filter: $searchTerm" custom-select-options="level1Options" ng-model="level1"></div>
<label>Level 2</label>
<div custom-select="g for g in nestedItemsLevel2 | filter: $searchTerm" ng-model="level2" cs-depends-on="level1"></div>
</div>
<!-- basic scripts -->
<!--[if !IE]> -->
<script src="http://code.jquery.com/jquery-2.1.1.min.js"></script>
<!-- <![endif]-->
<!--[if IE]>
<script src="http://code.jquery.com/jquery-1.11.1.min.js"></script>
<![endif]-->
<script src="http://maxcdn.bootstrapcdn.com/bootstrap/2.3.2/js/bootstrap.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.3/angular.min.js"></script>
<script src="js/customSelect.js"></script>
<script>
(function () {
var app = angular.module('Demo', ['AxelSoft']);
app.controller('DemoController', ['$scope', '$timeout', '$q', function ($scope, $timeout, $q) {
$scope.searchAsync = function (term) {
// No search term: return initial items
if (!term) {
return ['Item 1', 'Item 2', 'Item 3'];
}
var deferred = $q.defer();
$timeout(function () {
var result = [];
for (var i = 1; i <= 3; i++)
{
result.push(term + ' ' + i);
}
deferred.resolve(result);
}, 300);
return deferred.promise;
};
$scope.nestedItemsLevel1 = ['Item 1', 'Item 2', 'Item 3'];
$scope.level1 = $scope.nestedItemsLevel1[0];
$scope.level1Options = {
onSelect: function (item) {
var items = [];
for (var i = 1; i <= 5; i++) {
items.push(item + ': ' + 'Nested ' + i);
}
$scope.nestedItemsLevel2 = items;
}
};
$scope.nestedItemsLevel2 = [];
$scope.level1Options.onSelect($scope.nestedItemsLevel1[0]);
}]);
})();
</script>
</body>
https://docs.angularjs.org/api/ng/directive/select
There can be only one hard coded in a ngOption.

Define multiple html amd modules in one file

Is there any way to do a requirejs define in a text document? Or an html document?
I have a document filled with header and cell templates for a grid
<body>
<th data-fieldname="PatientPerson">Name <span data-bind="attr: { class: sortField() == 'PatientPerson' ? 'inline-block' : 'hide' }"></span></th>
<td><span class="glyphicon glyphicon-expand"></span><a data-bind="click: function () { $parent.loadReportSummary(PatientPerson.ID()) }"><span data-bind="text: PatientPerson.FullName"></span></a></td>
<th data-fieldname="StudyType">Study Type <span data-bind="attr: { class: sortField() == 'StudyType' ? 'inline-block' : 'hide' }"></span></th>
<td data-bind="text: StudyType"></td>
<th data-fieldname="ServiceDate">Service Date<span data-bind="attr: { class: sortField() == 'ServiceDate' ? 'inline-block' : 'hide' }"></span></th>
<td data-bind="text: ServiceDate"></td>
<th>Export Summary</th>
<td><a data-bind="click: function (data) { $parent.exportReportSummary(data, PatientPerson.ID, SummaryID, !StudyExported()) }">View</a></td>
<th>Print All Reports</th>
<td><a data-bind="click: function (data) { $parent.printAllReports('/PrintReports/Reports?summaryID=' + SummaryID) }">Print</a></td>
etc.......
</body>
In another module I have an array which determines which of these columns are used in a knockout computed observable. I was hoping that I could make each of these a module instead of parsing them using jquery, but I wanted them to all be in one file. I'm using the text plugin for requirejs, but there seems to be no way to declare each of these as a module inside of one file, and it seems wasteful to have to split each of these into separate files.
maybe something like
<!--export name:"PatientPerson" -->
<th data-fieldname="PatientPerson">Name <span data-bind="attr: { class: sortField() == 'PatientPerson' ? 'inline-block' : 'hide' }"></span></th>
<td><span class="glyphicon glyphicon-expand"></span><a data-bind="click: function () { $parent.loadReportSummary(PatientPerson.ID()) }"><span data-bind="text: PatientPerson.FullName"></span></a></td>
<!-- /export-->
Then referencing the module like
require('filename').PatientPerson;
I created a plugin where you can add a !define to the end of the require on some file, and it will make the spec suggested from the OP work. It works with the official text plugin for requirejs.
define(['text', 'module'], function (text, module) {
var exportRegExp = /<!--\s*?export[^>]*>([\s\S]*?)<!--\s*?\/export\s*?-->/g,
masterConfig = (module.config && module.config()) || {},
buildMap={};
text.export = function (content) {
var exports = null;
if (content) {
var matches = content.match(exportRegExp) || [],
match, _i, _len;
exports = matches.length ? {} : null;
for (_i = 0, _len = matches.length; _i < _len; _i++) {
match = matches[_i];
var exportName = match.match(/(<!--\s*?export\s*?name\:")(.*?)\"\s*?-->/).slice(-1)[0];
exports[exportName] = match.replace(/<!--\s*?export[^>]*>\n?/, '').replace(/<!--\s*?\/export\s*?-->/, '');
}
}
return exports;
};
var baseParseName = text.parseName;
text.parseName = function(name) {
var index, parseExport = false;
index = name.indexOf('!export');
if (index !== -1) {
parseExport = true;
name.split('!export').join('');
}
var out = baseParseName(name);
out["strip"] = { "export": parseExport, "strip": out["strip"] };
return out;
};
text.finishLoad = function (name, strip, content, onLoad) {
var exports = strip.export ? text.export(content,name) : content;
if (exports == null) content = strip.strip ? text.strip(content) : content;
else content = exports;
if (masterConfig.isBuild) {
buildMap[name] = content;
}
onLoad(content);
};
text.write = function (pluginName, moduleName, write, config) {
if (buildMap.hasOwnProperty(moduleName)) {
var content = text.jsEscape(buildMap[moduleName]);
write.asModule(pluginName + "!" + moduleName,
"define(function () { return '" +
content +
"';});\n");
}
};
});
The following will work
require([text!somefile!define],function(){})
This also works but it ignores the !strip command if any exports exist in the file.
require([text!somefile!strip!define],function(){})
then you can call
require(['someModuleNameInDefineComment'],function(){})
I have not handled strip with a beginning define in the header.
Here is a Gist