I'm on a angularjs application with jquery and can't seem to change the background color of some inputs. In short, when the user clicks "hide stage" text or "unhide stage" text, the input boxes background should change to white/lightgrey respectively. But the only thing that changes is the text between "hide stage" to "unhide stage" which is fine. Here is the angularjs file:
function stages($scope,$rootScope,$apiSrvc,$compile){
// defintions
// setting up stages statuses to angular array.
$scope.stages_visibilities = ['unhidden','unhidden','unhidden','unhidden','unhidden','unhidden','unhidden','unhidden'];
// changes the stage's status and it's visibility settings
$scope.setStageStatus = function(stageN,status){
$scope.stages_visibilities[stageN] = 'hidden';
if(status === 'unhide'){
$scope.stages_visibilities[stageN] = 'unhidden';
}
$scope.showStages();
};
// shows the stage rows.
$scope.showStages = function() {
// update(populate) the caption/value rows
$('#stages_rows').empty();
$nStages = parseInt($scope.nStages);
var stage_row = "";
for(var i=0; i < $nStages; i++){
stage_row += '<div class="stage_row">'+
'<input type="text" id="stage_caption_'+i+'" class="stg_caption" />'+
'<input type="number" id="stage_value_'+i+'" class="stg_val" />';
// show hide/unhide for this stage.
if($scope.stages_visibilities[i] === 'unhidden'){
stage_row += '<span id="stage_hide_unhide_'+i+'" class="hide_unhide_stg" ng-click="setStageStatus('+i+',\'hide\')">hide stage</span>';
// set row color to white.
$("#stage_caption_"+i).css("background-color","white");
$("#stage_value_"+i).css("background-color","white");
$("#stage_value_"+i).prop('disabled', false);
$("#stage_caption_"+i).prop('disabled', false);
}
else {
stage_row += '<span id="stage_hide_unhide_'+i+'" class="hide_unhide_stg" ng-click="setStageStatus('+i+',\'unhide\')">un-hide stage</span>';
// set row color to lightgrey.
$("#stage_caption_"+i).css("background-color","lightgrey");
$("#stage_value_"+i).css("background-color","lightgrey");
$("#stage_value_"+i).prop('disabled', true);
$("#stage_caption_"+i).prop('disabled', true);
}
stage_row += '</div>';
}
$("#stages_rows").append(stage_row);
// register new directives to angularjs
$compile($("#stages_rows").contents())($scope);
}
}
All the angular js pre-liminary stuff work fine, it's just that section of the code where it doesn't change the background colors of the input(#stage_caption_i and #stage_value_i) which is in the $scope.showStages function. I have no idea what is wrong.
Here is a more "Angular" approach to what you're trying to do. I would recommend setting aside your jQuery knowledge as it really should only be used in a very limited fashion in directives. As georgeawg mentioned you shouldn't do DOM manipulation in the controller. Rather you should modify the values of your model and then put all the view-specific stuff in the HTML.
A preferred method would be to make the hidden/visible status a property on the model, rather than maintaining a separate array with that data. Of course, you can still use an array, but then you have to write a method to check the value of the visibility in the corresponding array when you want to change a class or toggle the disabled status. Using a method like that will cause a lot of traffic between the controller and the view as the view calls back for each item. Probably not noticeable on a small sample like this, but could lead to performance issues on a larger view. Even if you are getting this data back from a database or some other storage on the back end you can still extend the model to include a visibility property on the client.
The id values aren't needed, but I've included them to show how to use $index in case you decide to go the route of storing visibility in a separate array. You could use the $index value from the ng-repeat to reference the proper element in the visibility array.
This separation of controller and view will help with long term maintainability and keep Angular and jQuery from stepping on each other as they both manipulate elements of the DOM.
angular.module('app', [])
.controller('ctrl', ($scope) => {
$scope.stages = [{
caption: 'Stage 0',
value: 0,
hidden: false
}, {
caption: 'Stage 1',
value: 1,
hidden: false
}, {
caption: 'Stage 2',
value: 2,
hidden: false
}, {
caption: 'Stage 3',
value: 3,
hidden: false
}, {
caption: 'Stage 4',
value: 4,
hidden: false
}, {
caption: 'Stage 5',
value: 5,
hidden: false
}, {
caption: 'Stage 6',
value: 6,
hidden: false
}, {
caption: 'Stage 7',
value: 7,
hidden: false
}];
// changes the stage's status and its visibility settings
$scope.setStageStatus = (stage) => {
stage.hidden = !stage.hidden;
};
});
.hidden {
background-color: lightgray;
}
.unhidden {
background-color: white;
}
.hide_unhide_stg {
cursor: pointer;
}
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.7.2/angular.min.js"></script>
<div ng-app="app" ng-controller="ctrl">
<div class="stage_row" ng-repeat="stage in stages">
<input type="text"
id="stage_caption_{{$index}}"
class="stg_caption"
ng-model="stage.caption"
ng-disabled="stage.hidden"
ng-class="{hidden: stage.hidden, unhidden: !stage.hidden}" />
<input type="number"
id="stage_value_{{$index}}"
class="stg_val"
ng-model="stage.value"
ng-disabled="stage.hidden"
ng-class="{hidden: stage.hidden, unhidden: !stage.hidden}" />
<span class="hide_unhide_stg"
ng-click="setStageStatus(stage)"><span ng-if="stage.hidden">un-</span>hide stage</span>
</div>
</div>
Related
document_table_Settings : function ()
{
return{
rowsPerPage: 5,
showNavigation: 'auto',
showColumnToggles: false,
fields: [
{key: 'para',label: 'Para',sortable: false},
{key: 'desc', label: 'Description',sortable: false},
{
key: 'rowId', label: 'Delete',sortable: false, fn: function (rowId, object) {
var html = "<button name='Del' id=" + rowId + " class='btn btn-danger'>Delete</button>"
return new Spacebars.SafeString(html);
}
},
{
key: 'rowId', label: 'Edit',sortable: false, fn: function (rowId, object) {
var html = "<button name='edit' id=" + rowId + " class='btn btn-warning'>Edit</button>"
return new Spacebars.SafeString(html);
}
}
]
};
}
I want to show description entries having show more and show less feature .As the description is long enough. so after 100 character it shows button to toggle.
If I understand you correctly, you are trying to only show the first 100 characters of the 'Description' column in the Reactive Table and then add some mechanism so that the user can click or rollover to see the entire 'Description' text.
There are a few ways to achieve this and I have provided two options below (in order of simplicity).
For a low tech rollover option, truncate the text to only show the first 100 characters, add an ellipsis (...) to the end of your text, then use the title property in a span element to show the full text on rollover.
First you will need to define a 'truncate' Template helper (I would make this a global helper so that you can use anywhere in your app).
Template.registerHelper('truncate', function(strValue, length) {
var len = DEFAULT_TRUNCATE_LENGTH;
var truncatedString = strValue;
if (length && length instanceof Number) {
len = length;
}
if (strValue.length > len) {
truncatedString = strValue.substr(1, len) + "...";
}
return truncatedString;
});
Then create a new Template for the column.
<template name="field_description">
<span title="{{data.description}}">{{truncate data.description}}</span>
</template>
And finally, change your Reactive Table configuration to use a Template.
fields: [
...,
{ key: 'desc', label: 'Description', tmpl: Template.field_description }
...,
];
For a slightly more complicated option, you can take a similar approach but add a clickable link that would show more or less detail. To get it to work you have to define a few Reactive Vars, define an event handler, and change your 'Description' Template accordingly. Here is a rough solution that should work.
Change your template like so.
<template name="field_description">
<span>{{truncatedDescription}}
{{#if showLink}}
{{linkState}}
{{/if}}
</span>
</template>
Then add the necessary logic to your field_description template (including an event handler).
import { Template } from 'meteor/templating';
import './field-description.html';
Template.field_descirption.onCreated(function() {
const MAX_LENGTH = 100;
this.description = new ReactiveVar(Template.currentData().description);
this.showMore = new ReactiveVar(true);
if (this.description.get().length > MAX_LENGTH) {
this.description.set(Template.currentData().description.substr(1, MAX_LENGTH));
}
this.showLink = () => {
return Template.currentData().description.length > MAX_LENGTH;
};
this.toggleTruncate = () => {
if (this.showMore.get()) {
this.description.set(Template.currentData().description);
this.showMore.set(false);
} else {
this.description.set(Template.currentData().description.substr(1, MAX_LENGTH));
this.showMore.set(true);
}
};
});
Template.field_descirption.helpers({
truncatedDescription: function() {
return Template.instance().description.get();
},
showLink: function() {
return Template.instance().showLink();
},
linkState: function() {
if (Template.instance().showMore.get()) {
return 'show more';
} else {
return 'show less';
}
};
});
Template.field_descirption.events({
'click .js-more-less'(event, instance) {
instance.toggleTruncate();
},
});
Lastly, make sure your Reactive Table config is still setup to use a Template for the field.
fields: [
...,
{ key: 'desc', label: 'Description', tmpl: Template.field_description }
...,
];
Note that the second option makes use of Meteor's Reactivity to solve the problem. Let me know if you need additional explanation on how the 2nd solution works.
That should do it!
I'm trying to build custom directive that will allow me to display questions in survey. Because I have multiple types of questions I thought about creating single directive and change it's template based on question type.
my directive:
directive('question', function($compile) {
var combo = '<div>COMBO - {{content.text}}</div>';
var radio = [
'<div>RADIO - {{content.text}}<br/>',
'<md-radio-group layout="row" ng-model="content.answer">',
'<md-radio-button ng-repeat="a in content.answers track by $index" ng-value="a.text" class="md-primary">{{a.text}}</md-radio-button>',
'</md-radio-group>',
'</div>'
].join('');
var input = [
'<div>INPUT - {{content.text}}<br/>',
'<md-input-container>',
'<input type="text" ng-model="content.answer" aria-label="{{content.text}}" required md-maxlength="10">',
'</md-input-container>',
'</div>'
].join('');
var getTemplate = function(contentType) {
var template = '';
switch (contentType) {
case 'combo':
template = combo;
break;
case 'radio':
template = radio;
break;
case 'input':
template = input;
break;
}
return template;
}
var linker = function(scope, element, attrs) {
scope.$watch('content', function() {
element.html(getTemplate(scope.content.type))
$compile(element.contents())(scope);
});
}
return {
//require: ['^^?mdRadioGroup','^^?mdRadioButton'],
restrict: "E",
link: linker,
scope: {
content: '='
}
};
})
Inside my main controller I have list of questions and after clicking button I'm setting current question that is assign to my directive.
Everything works fine for first questions, but after I set current question to radio type I get this error:
Error: [$compile:ctreq] Controller 'mdRadioGroup', required by
directive 'mdRadioButton', can't be found!
I've tried adding required to my directive as below, but it didn't helped.
require: ['^mdRadioGroup'],
I can't figure out whats going on, because I'm still new to angular.
I've created Plunker to show my issue: http://plnkr.co/edit/t0HJY51Mxg3wvvWrBQgv?p=preview
Steps to reproduce this error:
Open Plunker
Click Next button two times (to navigate to question 3)
See error in console
EDIT:
I've edited my Plunker so my questions model is visible. I'm able to select answers, even in questions that throw error-questions model is updating. But still I get error when going to question 3.
I'd just simply extend a base directive, and then have a specialized ones with different directive names too.
// <div b></div>
ui.directive('a', ... )
myApp.directive('b', function(aDirective){
return angular.extend({}, aDirective[0], { templateUrl: 'newTemplate.html' });
});
Code taken from https://github.com/angular/angular.js/wiki/Understanding-Directives#specialized-the-directive-configuration
Working Demo
There is no need to create and use a directive for your requirement.
You can just use angular templates and ng-include with condition.
You can just create three templates (each for combo, radio and input) on your page like this,
<script type="text/ng-template" id="combo">
<div>COMBO - {{content.text}}</div>
</script>
And include these templates in a div using ng-include.
<!-- Include question template based on the question -->
<div ng-include="getQuestionTemplate(question)">
Here, getQuestionTemplate() will return the id of the template which should be included in this div.
// return id of the template to be included on the html
$scope.getQuestionTemplate = function(content){
if(content.type == "combo"){
return 'combo';
}
else if (content.type == "radio"){
return 'radio';
}
else{
return 'input';
}
}
That's all. You are done.
Please feel free to ask me any doubt on this.
In case anyone is wondering, the problem is that the parent component's scope is used to compile each new element. Even when the element is removed, bindings on that scope still remain (unless overwritten), which may cause the errors OP saw (or even worse, memory leaks).
This is why one should take care of cleaning up when manipulating an element's HTML content imperatively, like this. And because this is tricky to get right, it is generally discouraged to do it. Most usecases should be covered by the built-in directives (e.g. ngSwitch for OP's case), which take care of cleaning up after themselves.
But you can get away with manually cleaning up in a simplified scenario (like the one here). In its simplest form, it involves creating a new child scope for each compiled content and destroying it once that content is removed.
Here is what it took to fix OP's plunker:
before
scope.$watch('content', function () {
element.html(getTemplate(scope.content.type));
$compile(element.contents())(scope);
});
after
var childScope;
scope.$watch('content', function () {
if (childScope) childScope.$destroy();
childScope = scope.$new();
element.html(getTemplate(scope.content.type));
$compile(element.contents())(childScope);
});
Here is the fixed version.
I played a little with your code and find that, the reason why the error occurred is because the 3rd question got more answers than the 2nd, so when you create the mdRadioGroup the first time it defines 4 $index answers and later for question 3 it go out of bound with 6 answers... So a non elegant solution is to create as many $index as the max answers to any question, the first time, show only the ones with text...
.directive('question', function($compile) {
var combo = '<div>COMBO - {{content.text}}</div>';
var radio = [
'<div>RADIO - {{content.text}}<br/>',
'<md-radio-group layout="row">',
'<md-radio-button ng-repeat="a in content.answers track by $index" ng-show={{a.text!=""}} value="{{a.text}}" class="md-primary">{{a.text}}</md-radio-button>',
'</md-radio-group>',
'</div>'
].join('');
var input = [
'<div>INPUT - {{content.text}}<br/>',
'<md-input-container>',
'<input type="text" ng-model="color" aria-label="{{content.text}}" required md-maxlength="10">',
'</md-input-container>',
'</div>'
].join('');
var getTemplate = function(contentType) {
var template = '';
switch (contentType) {
case 'combo':
template = combo;
break;
case 'radio':
template = radio;
break;
case 'input':
template = input;
break;
}
return template;
}
then change questions to have the max amount of answers every time in all questions:
$scope.questions = [{
type: 'radio',
text: 'Question 1',
answers: [{
text: '1A'
}, {
text: '1B'
}, {
text: '1C'
}, {
text: ''
}, {
text: ''
}, {
text: ''
}, {
text: ''
}]
}, {
type: 'input',
text: 'Question 2',
answers: [{
text: '2A'
}, {
text: '2B'
}, {
text: '2C'
}, {
text: ''
}, {
text: ''
}, {
text: ''
}, {
text: ''
}]
}, {
type: 'radio',
text: 'Question 3',
answers: [{
text: '3A'
}, {
text: '3B'
}, {
text: '3C'
}, {
text: '3D'
}, {
text: ''
}, {
text: ''
}, {
text: ''
}]
}, {
type: 'combo',
text: 'Question 4',
answers: [{
text: '4A'
}, {
text: '4B'
}, {
text: ''
}, {
text: ''
}, {
text: ''
}, {
text: ''
}, {
text: ''
}]
}];
The rest of the code is the same.
As I say before, no elegant and for sure there are better options, but could be a solution for now...
I create a check box dynamically (based on condition) on "create checkbox" button and added this checkbox in panel.
According to my condition, Only 1 checkbox will be added in panel and it's item id will be "a1".
When click on another button "checkedcheckbox" checkbox should be checked but it doesn't. When i get this checkbox by itemid on button click, then it show undefined in developer tools. If i get it by panel items then it doesn't show undefine but checkbox doesn't checked.
PROBLEM
Why checkbox show undefined if i get it by itemId ?
Why Checkbox is not checked if i am getting by panel items. It should be get in both ways by getting directly through itemid and panel items.
There is some problem on my account in sencha fiddler, that's why i can create a fiddler for you.
Code
Ext.onReady(function () {
var window = new Ext.Window({
id: 'grdWindow',
width: 400,
height: 500,
title: 'Grid Samples',
items: [
{
xtype: 'button',
text: 'Create checkbox',
handler: function () {
var obj1 = [
{ a: 1},
{ a: 2},
{ a: 3},
{ a: 4}
];
var obj2 = [
{ a: 1 },
{ a: 5 }
];
var i = 1;
var panel = Ext.getCmp('pnlTesting');
Ext.each(obj1, function (ob1) {
Ext.each(obj2, function (ob2) {
if (ob1.a == ob2.a) {
panel.add({
items:[
{
xtype: 'checkbox',
itemId: 'a'+i
}
]
});
return false;
}
});
});
}
},
{
xtype: 'panel',
id: 'pnlTesting',
height: 100,
renderTo: Ext.getBody()
},
{
xtype: 'button',
text: 'checkedcheckbox',
handler: function () {
Ext.getCmp('pnlTesting').items.items[0].items.items[0].checked = true;
//Ext.getCmp("a1").checked = true;
//Ext.getCmp('pnlTesting').doLayout();
}
}
]
}).show();
});
I would recommend learning the usage of .up() .down() .prev(), .next() and .query()
They are powerful tools to navigate through every component in your object structure without dealing with special item ids. You can also fetch components via config options like component.query('button[name="mybutton"]')
I made a (very) small example for your requirement in sencha fiddle: https://fiddle.sencha.com/#fiddle/sii
The problem is that if you want to change the state of the checkbox you should do it with setValue(true) and not checked=true.
{
xtype: 'button',
text: 'checkedcheckbox',
handler: function (btn) {
Ext.getCmp('pnlTesting').items.items[0].items.items[0].setValue(true);
}
}
And also, that's an ugly way of getting the component, i get it with down() and its itemId
Ext.getCmp('pnlTesting').down('#a1').setValue(true);
P.D: You should improve this selector because using id and getCmp() is not a good practice for ExtJS.
This is a follow up question that I got answered here: How can I programmatically set column filters?
I have a 188 line Ext.js view. In this view I extend Ext.grid.Panel and in this grid I have set the selModel like so ...
selModel: {
cellSelect: false, // Only support row selection.
type: 'spreadsheet' // Use the new "spreadsheet" style grid selection model.
},
On one of the columns, the Status column, I am programmatically setting the filter so that only rows that have the Status of Ready will appear when the page firsts renders. I have been doing this here in the code:
columns: [
...
{
text: 'Status',
dataIndex: 'status',
itemId: 'status',
renderer: function(value, metaData) {
var filter = this.up('panel').down('#status').filter;
if (!filter.menu) {
filter.createMenu();
filter.menu
.down('menuitem[value="Ready"]')
.setChecked(true);
}
metaData.tdStyle = (value == 'Ready') ?
'color:green;font-weight: bold' :
'color:red;font-style: italic'
return(value)
},
filter: 'list',
flex: 1,
},
... more columns ...
A helpful SO member pointed out that is not the most efficient place for the code that sets the filter as the code will be executed for each row in the grid.
I have tried adding an afterrender function like so ...
{
text: 'Status',
dataIndex: 'status',
itemId: 'status',
renderer: function(value, metaData) {
metaData.tdStyle = (value == 'Ready') ?
'color:green;font-weight: bold' :
'color:red;font-style: italic'
return(value)
},
filter: 'list',
flex: 1,
listeners: {
afterrender: function(value) {
Ext.Msg.alert('We have been rendered value is ' + value );
var filter = this.up('panel').down('#status').filter;
if (!filter.menu) {
filter.createMenu();
filter.menu
.down('menuitem[value="Ready"]')
.setChecked(true); //Uncaught TypeError: Cannot read property 'setChecked' of null
}
}},
... but that results in this error message, //Uncaught TypeError: Cannot read property 'setChecked' of null.
What am I doing wrong here? Do I need the listeners:? Am I not getting passed the data I think I am getting passed to my afterrender function? Should I defining a initComponent function?
UPDATE:
I changed my code to what DrakeES suggested, ...
{
text: 'Status',
dataIndex: 'status',
itemId: 'status',
renderer: function(value, metaData) {
metaData.tdStyle = (value == 'Ready') ?
'color:green;font-weight: bold' :
'color:red;font-style: italic'
return(value)
},
flex: 1,
filter: {
type: 'list',
value: 'Ready'
}
... but the result is this:
Where the animated loading image just sits there and spins. This prevents the user from be able to change the filter interactively. I wonder what it is I am doing wrong here?
I am programmatically setting the filter so that only rows that have
the Status of Ready will appear when the page firsts renders
What checking the filter's checkbox effectively does is setting filter on the store. Because you want the filter to be applied initially, it would be better to have it in the store config right away:
filters: [
{
id: 'x-gridfilter-status',
property: 'status',
value: 'Ready'
}
]
That way the grid view appear filtered in the first place — instead of initially showing all rows and only then filtering them out once the column menu renders and applies the filter. Note that having id: 'x-gridfilter-status' on the store's filter is required so that the column's filter picks it up instead of creating a duplicate.
Setting filter on the store, however, will not send feedback to the column filter menu, so the latter will remain unchecked unless you explicitly check it. Therefore, you still need an afterrender handler on either the grid or the column to make things look in sync.
A simple and elegant solution without listeners and stuff:
filter: {
type: 'list',
value: 'Ready'
}
Full working example: https://fiddle.sencha.com/#fiddle/prp
I am building a web app and am looking to convert the UI to use Knockout JS. I am a total noob in Knockout so please be kind!
Normally I would load an employee list (using PHP) and then if an employee is selected I would find the ID of that employee using JQuery and then make and AJAX call to my backend, fill in the result box and slide it down.
Is there a way to replicate this behavior in Knockout?
An boilerplate for you to start, uses jQuery and Knockout.
http://jsfiddle.net/5BHrc/6/
HTML
<ul data-bind="foreach: employees">
<li data-bind="css: {current: $data == $root.selected()}, click: $root.selected">
#<span data-bind="text: id"></span> - <span data-bind="text: name"></span>
</li>
</ul>
<div data-bind="slideVisible: ! loading(), html: employee_detail"></div>
CSS
.current {
background: blue;
color: white;
}
ul>li {
list-style: none;
}
JS
$(function() {
// for your requirment on sliding animation, this slideVisible is copied from http://knockoutjs.com/documentation/custom-bindings.html
ko.bindingHandlers.slideVisible = {
update: function(element, valueAccessor, allBindings) {
var value = valueAccessor();
var valueUnwrapped = ko.unwrap(value);
var duration = allBindings.get('slideDuration') || 400;
if (valueUnwrapped == true)
$(element).slideDown(duration); // Make the element visible
else
$(element).slideUp(duration); // Make the element invisible
}
};
var vm = {
employees: ko.observableArray([
// build your initial data in php
{id: 1, name: 'John'},
{id: 2, name: 'Tom'},
{id: 3, name: 'Lily'},
{id: 4, name: 'Bob'}
]),
selected: ko.observable(), // a placeholder for current selected
loading: ko.observable(false), // an indicator for ajax in progress
employee_detail: ko.observable() // holder for content from ajax return
};
// when user selects an employee, fire ajax
vm.selected.subscribe(function(emp) {
var emp_id = emp.id;
// ajax starts
vm.loading(true);
$.ajax('/echo/html/?emp_id='+emp_id, {
// just a simulated return from jsfiddle
type: 'POST',
data: {
html: "<b>Employee #" + emp_id + "</b><p>Details, bla bla...</p>",
delay: 0.2
},
success: function (content) {
// update employee_detail
vm.employee_detail(content);
},
complete: function() {
// ajax finished
vm.loading(false);
}
});
});
ko.applyBindings(vm);
});
This sounds similar to the drill down that happens with folders and emails in this knockout tutorial.