I am currently building a form builder with vue3 composition API. The user can add in different types of inputs like text, radio buttons etc into the form before saving the form. The saved form will then render with the appropriate HTML inputs. The user can edit the name of the question, eg Company Name <HTML textInput.
Currently, when the user adds an input type eg,text, the type is saved into an ordered array. I run a v-for through the ordered array and creating a custom component formComponent, passing in the type.
My formComponent renders out a basic text input for the user to edit the name of the question, and a place holder string for where the text input will be displayed. My issue is in trying to save the question text from the parent.
<div v-if="type=='text'">
<input type="text" placeholder="Key in title"/>
<span>Input field here</span>
</div>
I have an exportForm button in the parent file that when pressed should ideally return an ordered array of toString representations of all child components. I have tried playing with $emit but I have issue triggering the $emit on all child components from the parent; if I understand, $emit was designed for a parent component to listen to child events.
I have also tried using $refs in the forLoop. However, when I log the $refs they give me the div elements.
<div v-for="item in formItems" ref="formComponents">
<FormComponent :type="item" />
</div>
The ideal solution would be to define a method toString() inside each of the child components and have a forLoop running through the array of components to call toString() and append it to a string but I am unable to do that.
Any suggestions will be greatly appreciated!
At first:
You don't really need to access the child components, to get their values. You can bind them dynamically on your data. I would prefer this way, since it is more Vue conform way to work with reactive data.
But I have also implemented the other way you wanted to achieve, with accessing the child component's methods getValue().
I would not suggest to use toString() since it can be confused with internal JS toString() function.
In short:
the wrapping <div> is not necessary
the refs should be applied to the <FormComponents> (see Refs inside v-for)
this.$refs.formComponents returns the Array of your components
FormComponent is used here as <form-components> (see DOM Template Parsing Caveats)
The values are two-way bound with Component v-model
Here is the working playground with the both ways of achieving your goal.
Pay attention how the values are automatically changing in the FormItems data array.
const { createApp } = Vue;
const FormComponent = {
props: ['type', 'modelValue'],
emits: ['update:modelValue'],
template: '#form-component',
data() {
return { value: this.modelValue }
},
methods: {
getValue() {
return this.value;
}
}
}
const App = {
components: { FormComponent },
data() {
return {
formItems: [
{ type: 'text', value: null },
{ type: 'checkbox', value: false }
]
}
},
methods: {
getAllValues() {
let components = this.$refs.formComponents;
let values = [];
for(var i = 0; i < components.length; i++) {
values.push(components[i].getValue())
}
console.log(`values: ${values}`);
}
}
}
const app = createApp(App)
app.mount('#app')
#app { line-height: 2; }
[v-cloak] { display: none; }
label { font-weight: bold; }
th, td { padding: 0px 8px 0px 8px; }
<div id="app">
<label>FormItems:</label><br/>
<table border=1>
<thead><tr><th>#</th><th>Item Type:</th><th>Item Value</th></tr></thead>
<tbody><tr v-for="(item, index) in formItems" :key="index">
<td>{{index}}</td><td>{{item.type}}</td><td>{{item.value}}</td>
</tr></tbody>
</table>
<hr/>
<label>FormComponents:</label>
<form-component
v-for="(item, index) in formItems"
:type="item.type" v-model="item.value" :key="index" ref="formComponents">
</form-component>
<button type="button" #click="getAllValues">Get all values</button>
</div>
<script src="https://unpkg.com/vue#3/dist/vue.global.prod.js"></script>
<script type="text/x-template" id="form-component">
<div>
<label>type:</label> {{type}},
<label>value:</label> <input :type='type' v-model="value" #input="$emit('update:modelValue', this.type=='checkbox' ? $event.target.checked : $event.target.value)" />
</div>
</script>
I have a <v-text-field> which collects user e-mail address and I need to add ym-record-keys CSS class to <input> element according to Yandex Metrica guideline. So I tried to add it like this but it adds class to the parent div. How can I add ym-record-keys class to HTML input element?
<v-text-field
v-model="item.customer.mail"
type="email"
:rules="emailRules"
hide-details="auto"
class="ym-record-keys"
solo
>
</v-text-field>
I need to achieve this output:
<input id="input-1055" type="email" class="ym-record-keys">
I needed this ability as well because another plugin I was using expects a certain class to exist on my input elements.
In order to achieve this I created a simple wrapper around Vuetify's v-text-field that adds the ability to pass in a class to the input element:
Create a TextInput.vue component:
<script>
import { VTextField } from 'vuetify/lib';
export default {
name: 'text-input',
extends: VTextField,
props: {
inputClass: {
note: 'A class that gets applied to the input',
},
},
mounted() {
// Applying the `class` to the input
// element below if it has values
const inputEl = this.$el.querySelector('input');
if (this.inputClass) {
inputEl.classList.add(this.inputClass);
}
},
};
</script>
Then used like this:
<text-input v-model="myModel" input-class="my-css-class" />
Which results in your input looking like this:
<input id="idGeneratedFromVuetify" type="text" class="my-css-class">
You can modify this to fit any other attributes you may want, or you could make it more dynamic if your use case needs that by using the spread operator with props.
You can try the following to bind the class
:class=["ym-record-keys"]
or
:class="'ym-record-keys'"
How can I make the <vue-autosuggest> input look like the input from BootstrapVue (<b-input>)?
To apply the form-control class to vue-autosuggest's inner <input>, as required for the Bootstrap <input> styles, you could use the inputProps prop (automatically applied to the <input>):
<template>
<vue-autosuggest :input-props="inputProps">
...
</vue-autosuggest>
</template>
<script>
export default {
data() {
return {
inputProps: {
class: 'form-control',
}
}
}
}
</script>
Interestingly, that class used to be added by default in vue-autosuggest 1.x, but was removed in 2.x.
demo
You need to modify the the <input> tag of the vue-autosuggest component to include the class form-control from Vue-Bootstrap.
You will not be able to add this class directly to the component as the component wraps the input within a div block. Bootstrap CSS requires the the element to be of type input to properly match the CSS selectors.
If we look here https://github.com/darrenjennings/vue-autosuggest/blob/master/src/Autosuggest.vue at the component itself we see
<input
:type="internal_inputProps.type"
:value="internalValue"
:autocomplete="internal_inputProps.autocomplete"
:class="[isOpen ? `${componentAttrPrefix}__input--open` : '', internal_inputProps['class']]"
v-bind="internal_inputProps"
aria-autocomplete="list"
:aria-activedescendant="isOpen && currentIndex !== null ? `${componentAttrPrefix}__results-item--${currentIndex}` : ''"
:aria-controls="`${componentAttrIdAutosuggest}-${componentAttrPrefix}__results`"
#input="inputHandler"
#keydown="handleKeyStroke"
v-on="listeners"
>
This must be modified in your local version to include the bootstrap class.
Is it possible to bind a state (attribute) of a paper-checkbox [checked|unchecked] dynamically to an attribute like [readonly|disabled] inside a paper-input element? This is my implementation so far:
<template repeat="{{item in lphasen}}">
<div center horizontal layout>
<paper-checkbox unchecked on-change="{{checkStateChanged}}" id="{{item.index}}"></paper-checkbox>
<div style="margin-left: 24px;" flex>
<h4>{{item.name}}</h4>
</div>
<div class="container"><paper-input disabled floatingLabel id="{{item.index}}" label="LABEL2" value="{{item.percent}}" style="width: 120px;"></paper-input></div>
</div>
</template>
The behavior should be as follow:
When the user uncheck a paper-checkbox, then the paper-input element in the same row should be disabled and/or readonly and vice versa. Is it possible to directly bind multiple elements with double-mustache or do I have to iterate the DOM somehow to manually set the attribute on the paper-input element? If YES, could someone explain how?
Another way to bind the checked state of the paper-checkbox.
<polymer-element name="check-input">
<template>
<style>
#checkbox {
margin-left: 1em;
}
</style>
<div center horizontal layout>
<div><paper-input floatingLabel label="{{xlabel}}" value="{{xvalue}}" disabled="{{!xenable}}" type="number" min="15" max="200"></paper-input></div>
<div><paper-checkbox id="checkbox" label="Enable" checked="{{xenable}}"></paper-checkbox></div>
</div>
</template>
<script>
Polymer('check-input', {
publish:{xenable:true, xvalue:'',xlabel:''}
});
</script>
</polymer-element>
<div>
<check-input xenable="true" xvalue="100" xlabel="Weight.1"></check-input>
<check-input xenable="false" xvalue="185" xlabel="Weight.2"></check-input>
</div>
jsbin demo http://jsbin.com/boxow/
My preferred approach would be to refactor the code to create a Polymer element responsible for one item. That way, all of the item specific behaviour is encapsulated in one place.
Once that is done, there are a couple ways of doing this.
The easiest would be to simply create an on-tap event for the check box that toggles the value of a property and sets the disabled attribute accordingly.
<paper-checkbox unchecked on-tap="{{checkChanged}}"></paper-checkbox>
//Other markup for item name display
<paper-input disabled floatingLabel id="contextRelevantName" style="width:120 px;"></paper-input>
One of the benefits of putting this into it's own polymer element is that you don't have to worry about unique id's anymore. The control id's are obfuscated by the shadowDOM.
For the scripting, you would do something like this:
publish: {
disabled: {
value: true,
reflect: false
}
}
checkChanged: function() {
this.$.disabled= !this.$.disabled;
this.$.contextRelevantName.disabled = this.$.disabled;
}
I haven't tested this, so there might be some tweaks to syntax and what have you, but this should get you most of the way there.
Edit
Based on the example code provided in your comment below, I've modified your code to get it working. The key is to make 1 element that contains an either row, not multiple elements that contain only parts of the whole. so, the code below has been stripped down a little bit to only include the check box and the input it is supposed to disable. You can easily add more to the element for other parts of your item displayed.
<polymer-element name="aw-leistungsphase" layout vertical attributes="label checked defVal contractedVal">
<template>
<div center horizontal layout>
<div>
<paper-checkbox checked on-tap="{{checkChanged}}" id="checkbox" label="{{label}}"></paper-checkbox>
</div>
<div class="container"><paper-input floatingLabel id="contractedInput" label="Enter Value" value="" style="width: 120px;"></paper-input></div>
</div>
</template>
<script>
Polymer('aw-leistungsphase', {
publish: {
/**
* The label for this input. It normally appears as grey text inside
* the text input and disappears once the user enters text.
*
* #attribute label
* #type string
* #default ''
*/
label: '',
defVal : 0,
contractedVal : 0
},
ready: function() {
// Binding the project to the data-fields
this.prj = au.app.prj;
// i18n mappings
this.i18ndefLPHLabel = au.xlate.xlate("hb.defLPHLabel");
this.i18ncontractedLPHLabel = au.xlate.xlate("hb.contractedLPHLabel");
},
observe : {
'contractedVal' : 'changedLPH'
},
changedLPH: function(oldVal, newVal) {
if (oldVal !== newVal) {
//this.prj.hb.honlbl = newVal;
console.log("GeƤnderter Wert: " + newVal);
}
},
checkChanged: function(e, detail, sender) {
console.log(sender.label + " " + sender.checked);
if (!this.$.checkbox.checked) {
this.$.contractedInput.disabled = 'disabled';
}
else {
this.$.contractedInput.disabled = '';
}
console.log("Input field disabled: " + this.$.contractedInput.disabled);
}
});
</script>
</polymer-element>
Q1. Suppose I want to alter the look of each "item" that a user marks for deletion before the main "delete" button is pressed. (This immediate visual feedback should eliminate the need for the proverbial "are you sure?" dialog box.) The user will check checkboxes to indicate which items should be deleted. If a checkbox is unchecked, that item should revert back to its normal look.
What's the best way to apply or remove the CSS styling?
Q2. Suppose I want to allow each user to personalize how my site is presented. E.g., select from a fixed set of font sizes, allow user-definable foreground and background colors, etc.
What's the best way to apply the CSS styling the user selects/inputs?
Angular provides a number of built-in directives for manipulating CSS styling conditionally/dynamically:
ng-class - use when the set of CSS styles is static/known ahead of time
ng-style - use when you can't define a CSS class because the style values may change dynamically. Think programmable control of the style values.
ng-show and ng-hide - use if you only need to show or hide something (modifies CSS)
ng-if - new in version 1.1.5, use instead of the more verbose ng-switch if you only need to check for a single condition (modifies DOM)
ng-switch - use instead of using several mutually exclusive ng-shows (modifies DOM)
ng-disabled and ng-readonly - use to restrict form element behavior
ng-animate - new in version 1.1.4, use to add CSS3 transitions/animations
The normal "Angular way" involves tying a model/scope property to a UI element that will accept user input/manipulation (i.e., use ng-model), and then associating that model property to one of the built-in directives mentioned above.
When the user changes the UI, Angular will automatically update the associated elements on the page.
Q1 sounds like a good case for ng-class -- the CSS styling can be captured in a class.
ng-class accepts an "expression" that must evaluate to one of the following:
a string of space-delimited class names
an array of class names
a map/object of class names to boolean values
Assuming your items are displayed using ng-repeat over some array model, and that when the checkbox for an item is checked you want to apply the pending-delete class:
<div ng-repeat="item in items" ng-class="{'pending-delete': item.checked}">
... HTML to display the item ...
<input type="checkbox" ng-model="item.checked">
</div>
Above, we used ng-class expression type #3 - a map/object of class names to boolean values.
Q2 sounds like a good case for ng-style -- the CSS styling is dynamic, so we can't define a class for this.
ng-style accepts an "expression" that must evaluate to:
an map/object of CSS style names to CSS values
For a contrived example, suppose the user can type in a color name into a texbox for the background color (a jQuery color picker would be much nicer):
<div class="main-body" ng-style="{color: myColor}">
...
<input type="text" ng-model="myColor" placeholder="enter a color name">
Fiddle for both of the above.
The fiddle also contains an example of ng-show and ng-hide. If a checkbox is checked, in addition to the background-color turning pink, some text is shown. If 'red' is entered in the textbox, a div becomes hidden.
I have found problems when applying classes inside table elements when I had one class already applied to the whole table (for example, a color applied to the odd rows <myClass tbody tr:nth-child(even) td>). It seems that when you inspect the element with Developer Tools, the element.style has no style assigned. So instead of using ng-class, I have tried using ng-style, and in this case, the new CSS attribute does appear inside element.style. This code works great for me:
<tr ng-repeat="element in collection">
[...amazing code...]
<td ng-style="myvar === 0 && {'background-color': 'red'} ||
myvar === 1 && {'background-color': 'green'} ||
myvar === 2 && {'background-color': 'yellow'}">{{ myvar }}</td>
[...more amazing code...]
</tr>
Myvar is what I am evaluating, and in each case I apply a style to each <td> depending on myvar value, that overwrites the current style applied by the CSS class for the whole table.
UPDATE
If you want to apply a class to the table for example, when visiting a page or in other cases, you can use this structure:
<li ng-class="{ active: isActive('/route_a') || isActive('/route_b')}">
Basically, what we need to activate a ng-class is the class to apply and a true or false statement. True applies the class and false doesn't. So here we have two checks of the route of the page and an OR between them, so if we are in /route_a OR we are in route_b, the active class will be applied.
This works just having a logic function on the right that returns true or false.
So in the first example, ng-style is conditioned by three statements. If all of them are false, no style is applied, but following our logic, at least one is going to be applied, so, the logic expression will check which variable comparison is true and because a non empty array is always true, that will left an array as return and with only one true, considering we are using OR for the whole response, the style remaining will be applied.
By the way, I forgot to give you the function isActive():
$rootScope.isActive = function(viewLocation) {
return viewLocation === $location.path();
};
NEW UPDATE
Here you have something I find really useful. When you need to apply a class depending on the value of a variable, for example, an icon depending on the contents of the div, you can use the following code (very useful in ng-repeat):
<i class="fa" ng-class="{ 'fa-github' : type === 0,
'fa-linkedin' : type === 1,
'fa-skype' : type === 2,
'fa-google' : type === 3 }"></i>
Icons from Font Awesome
This works well when ng-class can't be used (for example when styling SVG):
ng-attr-class="{{someBoolean && 'class-when-true' || 'class-when-false' }}"
(I think you need to be on latest unstable Angular to use ng-attr-, I'm currently on 1.1.4)
I have published an article on working with AngularJS+SVG. It talks about this issue and numerous others. http://www.codeproject.com/Articles/709340/Implementing-a-Flowchart-with-SVG-and-AngularJS
span class="circle circle-{{selectcss(document.Extension)}}">
and code
$scope.selectcss = function (data) {
if (data == '.pdf')
return 'circle circle-pdf';
else
return 'circle circle-small';
};
css
.circle-pdf {
width: 24px;
height: 24px;
font-size: 16px;
font-weight: 700;
padding-top: 3px;
-webkit-border-radius: 12px;
-moz-border-radius: 12px;
border-radius: 12px;
background-image: url(images/pdf_icon32.png);
}
This solution did the trick for me
<a ng-style="{true: {paddingLeft: '25px'}, false: {}}[deleteTriggered]">...</a>
You can use ternary expression. There are two ways to do this:
<div ng-style="myVariable > 100 ? {'color': 'red'} : {'color': 'blue'}"></div>
or...
<div ng-style="{'color': (myVariable > 100) ? 'red' : 'blue' }"></div>
Another option when you need a simple css style of one or two properties:
View:
<tr ng-repeat="element in collection">
[...amazing code...]
<td ng-style="{'background-color': getTrColor(element.myvar)}">
{{ element.myvar }}
</td>
[...more amazing code...]
</tr>
Controller:
$scope.getTrColor = function (colorIndex) {
switch(colorIndex){
case 0: return 'red';
case 1: return 'green';
default: return 'yellow';
}
};
See the following example
<!DOCTYPE html>
<html ng-app>
<head>
<title>Demo Changing CSS Classes Conditionally with Angular</title>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.0.7/angular.min.js"></script>
<script src="res/js/controllers.js"></script>
<style>
.checkboxList {
border:1px solid #000;
background-color:#fff;
color:#000;
width:300px;
height: 100px;
overflow-y: scroll;
}
.uncheckedClass {
background-color:#eeeeee;
color:black;
}
.checkedClass {
background-color:#3ab44a;
color:white;
}
</style>
</head>
<body ng-controller="TeamListCtrl">
<b>Teams</b>
<div id="teamCheckboxList" class="checkboxList">
<div class="uncheckedClass" ng-repeat="team in teams" ng-class="{'checkedClass': team.isChecked, 'uncheckedClass': !team.isChecked}">
<label>
<input type="checkbox" ng-model="team.isChecked" />
<span>{{team.name}}</span>
</label>
</div>
</div>
</body>
</html>
As of AngularJS v1.2.0rc, ng-class and even ng-attr-class fail with SVG elements (They did work earlier, even with normal binding inside the class attribute)
Specifically, none of these work now:
ng-class="current==this_element?'active':' ' "
ng-attr-class="{{current==this_element?'active':' '}}"
class="class1 class2 .... {{current==this_element?'active':''}}"
As a workaround, I've to use
ng-attr-otherAttr="{{current==this_element?'active':''}}"
and then style using
[otherAttr='active'] {
... styles ...
}
One more (in the future) way to conditionally apply style is by conditionally creating scoped style
<style scoped type="text/css" ng-if="...">
</style>
But nowadays only FireFox supports scoped styles.
There is one more option that I recently discovered that some people may find useful because it allows you to change a CSS rule within a style element - thus avoiding the need for repeated use of an angular directive such as ng-style, ng-class, ng-show, ng-hide, ng-animate, and others.
This option makes use of a service with service variables which are set by a controller and watched by an attribute-directive I call "custom-style". This strategy could be used in many different ways, and I attempted to provide some general guidance with this fiddle.
var app = angular.module('myApp', ['ui.bootstrap']);
app.service('MainService', function(){
var vm = this;
});
app.controller('MainCtrl', function(MainService){
var vm = this;
vm.ms = MainService;
});
app.directive('customStyle', function(MainService){
return {
restrict : 'A',
link : function(scope, element, attr){
var style = angular.element('<style></style>');
element.append(style);
scope.$watch(function(){ return MainService.theme; },
function(){
var css = '';
angular.forEach(MainService.theme, function(selector, key){
angular.forEach(MainService.theme[key], function(val, k){
css += key + ' { '+k+' : '+val+'} ';
});
});
style.html(css);
}, true);
}
};
});
well i would suggest you to check condition in your controller with a function returning true or false .
<div class="week-wrap" ng-class="{today: getTodayForHighLight(todayDate, day.date)}">{{day.date}}</div>
and in your controller check the condition
$scope.getTodayForHighLight = function(today, date){
return (today == date);
}
One thing to watch is - if the CSS style has dashes - you must remove them. So if you want to set background-color, the correct way is:
ng-style="{backgroundColor:myColor}"
Here's how i conditionally applied gray text style on a disabled button
import { Component } from '#angular/core';
#Component({
selector: 'my-app',
styleUrls: [ './app.component.css' ],
template: `
<button
(click)='buttonClick1()'
[disabled] = "btnDisabled"
[ngStyle]="{'color': (btnDisabled)? 'gray': 'black'}">
{{btnText}}
</button>`
})
export class AppComponent {
name = 'Angular';
btnText = 'Click me';
btnDisabled = false;
buttonClick1() {
this.btnDisabled = true;
this.btnText = 'you clicked me';
setTimeout(() => {
this.btnText = 'click me again';
this.btnDisabled = false
}, 5000);
}
}
Here's a working example:
https://stackblitz.com/edit/example-conditional-disable-button?file=src%2Fapp%2Fapp.component.html