I'm having an issue with component binding and state.
This is my html template:
<div class="ts-panel content">
<!--ko with: states.createState-->
<div data-bind="component: 'customer-create'">Testing CreateState</div>
<!--/ko-->
<!--ko with: states.lookupState-->
<div data-bind="component: 'customer-search'">Testing LookupState</div>
<!--/ko-->
</div>
This is my javascript
var myDataModel = function () {
var self = this;
self.states = {};
self.states.createState = ko.observable(true);
self.states.lookupState = ko.observable(false);
self.states.currentState = ko.observable(self.states.createState);
self.states.changeState = function (state) {
var currentState = self.states.currentState();
currentState(false);
self.states.currentState(state);
state(true);
}
};
return myDataModel;
I'm using another script to control which state I'm in by binding click events to certain buttons.
The problem I'm running into is that when I change the current state, the component bindings reset the state of the component. Eg. on the customer-create component, I fill out a form, then change to the lookupState, then change back to the createState, the form values are gone.
I think this is happening because the components are getting wiped out and recreated every time.
I also think that one solution to this is to store everything at the root level (i.e. the component that stores the states) and pass that all down when required to the individual components. However, I'd really like to keep the component-specific information inside those components.
Is there a way to store the state of the components or maybe store the components in a variable and bind to it that way?
From the documentations:
If the expression you supply involves any observable values, the expression will be re-evaluated whenever any of those observables change. The descendant elements will be cleared out, and a new copy of the markup will be added to your document and bound in the context of the new value.
The behaviour is same for the if binding as well. You could use the visible binding for this. This just hides and shows the div without actually removing it from the DOM. There is no containerless control flow syntax for visible. So, you'd have to add it to the div
<div data-bind="component:'customer-create', visible: states.createState">Testing CreateState</div>
Related
I would like to attach a ref attribute to an HTML element that has a custom directive.
Let's say I have a directive named v-custom and whenever it is used on an element, I would like ref="elem" to be added to the element.
I have tried using directives like so:
Vue.directive('custom', {
inserted(el) {
el.setAttribute("ref", "elem");
}
});
And in one of my components I have this:
<span v-custom>Testing</span>
And when I view it in a web page I can inspect that span element and see that it has the ref attribute but when I inspect the refs of the component it belongs to it says that it contains no "elem" ref.
However, If I add the ref tag myself like so:
<span v-custom ref="elem">Testing</span>
Then it works as intended and I can see the "elem" ref in the console.
Is there any way to get my use case working or is this intended behavior?
As #skirtle noted, ref is written as a normal DOM attribute in the vue template code, but is handled differently when parsed. A vue component instance/view model/"vm" has an object vm.$refs which maps keys to DOM elements. We can modify this object ourself. The issue then is how to get the parent vm from within the directive (we already got the DOM element el).
Looking at the documentation for custom directives https://v2.vuejs.org/v2/guide/custom-directive.html#Directive-Hook-Arguments, we can see that the third argument is a "vnode" reference, and looking at its documentation, we can see that vnode.context references the container vm; thus:
Vue.directive('my-directive', {
inserted (el, binding, vnode) {
console.log('directive inserted')
const refKey = "s2"
vnode.context.$refs[refKey] = el // set it
}
})
Vue.component('my-component', {
template: '#my-component-template',
replace: false,
props: {text: String},
mounted () {
console.log('component mounted')
this.$refs.s1.innerText = 's1 ref working'
this.$refs.s2.innerText = 's2 ref working' // use it
}
});
new Vue({
el: '#app',
data: {
status: "initialized",
},
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
hello
<my-component :text="status"></my-component>
</div>
<script type="text/x-template" id="my-component-template">
<div>
{{text}}, <!-- to illustrate props data passing working as usual -->
<span ref="s1"></span>, <!-- to illustrate a normal ref -->
<span class="nested"> <!-- to illustrate vnode.context doesn't just get the parent node -->
<span v-my-directive></span>
</span>
</div>
</script>
Running this example, we can see that the v-my-directive successfully modifies vm.$refs.s2 to reference the DOM-element with the directive, before the mounted function in the vm is run, where we can use the reference.
Beware that you probably would like some logic to not overwrite the ref if more that one elements contains the directive.
Happy coding!
Dynamically swapping BODY content using jQuery html function works as expected with 'static' content.
But if forms are being used, current state of inputs is lost.
The jQuery detach function, which should keep page state, seems to be blanking out the whole page.
The following code is the initial idea using jQuery html but of course the text input value will always empty.
function swap1( ) {
$("body").html('<button onclick="swap2();">SWAP2</button><input type="text" placeholder="swap2"/>');
}
function swap2( ) {
$("body").html('<button onclick="swap1();">SWAP1</button><input type="text" placeholder="swap1"/>');
}
With not knowing what form inputs there are, how would one swap in and out these forms in the BODY and keep track of the form states?
Demo of two text inputs which should keep state when they come back into the BODY:
https://jsfiddle.net/g7hksfne/3/
Edit: missunderstood use case. This covers saving the state while manipulating the DOM, instead of switching between different states.
Instead of replacing the <body>'s content by serializing and parsing HTML, learn how to use the DOM. Only replace the parts of the DOM you actually change, so the rest of it can keep its state (which is stored in the DOM).
In your case: you might want to use something like this:
function swap1( ) {
document.querySelector("button").onclick = swap2;
document.querySelector("button").textContent = "SWAP2";
document.querySelector("input").placeholder = "swap2";
}
function swap2( ) {
document.querySelector("button").onclick = swap1;
document.querySelector("button").textContent = "SWAP1";
document.querySelector("input").placeholder = "swap1";
}
<button onclick="swap1();">SWAP1</button><input type="text" placeholder="swap1"/>
(This is not optimized and should only serve as an example.)
Put the content you want to save in a node below <body>, like a simple ยด` if you don't already have a container. When you want to save and replace the container, use something like:
var saved_container = document.body.removeChild(document.querySelector("#app_container");
// or document.getElementById/getElementsByClassName, depends on container
The element is now detached and you can add your secondary to document.body. When you want to get back, save the secondary content (without overwriting the first container of course), then reattach the primary content it with:
document.body.appendChild(savedContainer);
I'm very confused about how to properly tie components together.
I have two components registered globally:
Vue.component('product-welcome-component', {
template: '#product-welcome-template',
props: ['showModal'],
onCreate(){
showModal = false;
}
});
Vue.component('product-create-modal-component', {
template: '#create-modal-template'
});
In the parent's template I included the child component like this:
<template id="product-welcome-template">
<div class="welcome-wrapper">
<div class="purpose-title"><h1 class="welcome-text">Welcome to Product Hub</h1></div>
<div class="purpose-create-btn"><button ##click="showModal = true" class="btn btn-primary btn-success create-btn">Create New Product</button></div>
<product-create-modal-component v-if="showModal"></product-create-modal-component>
</div>
</template>
The problem is (one of them) is that my create-modal-component is always showing, regardless of the value of showModal, in fact i can put in v-if="1 === 2" it would still show.
I'm sure this is not the right way of registering parent / child components but I can't seem to find a proper example. Mostly what i see that the parent is the app instance and it has a child of 'child' component and then they can communicate.
I have a feeling that including the child component in the parent's template is bad practice as it makes the parent strongly coupled.
Any help would be appreciated, thank you!
You are having showModal as props to product-welcome-component, but you are trying to set it false in created, but you have to use this in created to access showModal, like following:
Vue.component('product-welcome-component', {
template: '#product-welcome-template',
props: ['showModal'],
onCreate(){
this.showModal = false;
}
});
However you are saying product-create-modal-component shows even you do v-if="1 === 2", which should not be the case Can you create a fiddle of your case.
I have the following structures
<div ng-controller='ctrlA'>
<button ng-click='updateFactoryData(data)'></button>
<custom-dir>Custom directive with ctrlB</custom-dir>
<custom-dir>Custom directive with ctrlB</custom-dir>
<custom-dir>Custom directive with ctrlB</custom-dir>
...
<other-custom-dir>Custom directive with ctrlC</other-custom-dir>
<other-custom-dir>Custom directive with ctrlC</other-custom-dir>
<other-custom-dir>Custom directive with ctrlC</other-custom-dir>
...
</div>
I have a dataFactory, which when user clicks the button, the updated data will store in dataFactory, the dataFactory is injected to both ctrlB and ctrlC.
The problem is, when data is updated after clicking the button, the changes do not reflect on both custom-dir and other-custom-dir, is there any way ctrlB and ctrlC's scope value automatically reflect the changes made in scope under ctrlA?
Thanks very much for the help!
Thanks all for the kind help!! I finally figure out why the change cannot be reflected, I watch the object parameter to see if there are changes, but watch only monitor the object reference instead of the actual content, after deep watching the object parameter, I can finally get the watch work! Thanks for all the help
<custom-dir para="{data: data}"></custom-dir>
Then in directive's link function
scope.$watch('para.data', function(n,o){
alert('changed')
}, true); //>> The true is important!!
The fact that you need to share scope is a bit weird knowing that you have a parent controller that can contain your $scope item
Pass the value to attribute in the <custom-dir> and <other-custom-dir>
<custom-dir data="data"> </custom-dir>
Observe the change from the directive
attrs.$observe('data', function(value){
// call your directive controller here ...
})
The Benefit :
By passing data from attribute, the directive do not have to know about the controller/the source of the data, so, this reduce dependecy.
It's considered a best practice to use "controllerAs" and no longer use $scope.
By writing:
<div ng-controller='ctrlA as a'>
<button ng-click='a.updateFactoryData(data)'></button>
<!-- ... -->
</div>
and:
function ctrlA () {
this.data = [];
this.updateFactoryData = function (data) {
//...
}
}
You access your controller's data with a.data everywhere inside your div.
Basically what you require is here is share the factory data changes in your directives. So you can bind your desired factory data using different approaches based on your requirements(i.e. # attribute,= 2 way model, & expression bindings) in directives and than watch those variables in directives link functions (i.e. ideal place where you needs to handle those watch conditions) for changes so once anything will update on factory (through button click), it will automatically propagated in your directives. Here is the example to do different bindings.
var myModule = angular.module('myModule', [])
.directive('myComponent', function () {
return {
restrict:'E',
scope:{
/* NOTE: Normally I would set my attributes and bindings
to be the same name but I wanted to delineate between
parent and isolated scope. */
isolatedAttributeFoo:'#attributeFoo',
isolatedBindingFoo:'=bindingFoo',
isolatedExpressionFoo:'&'
}
};
})
<my-component attribute-foo="{{foo}}" binding-foo="foo"
isolated-expression-foo="updateFoo(newFoo)" >
<h2>Attribute</h2>
<div>
<strong>set:</strong> <input ng-model="isolatedAttributeFoo">
<i>// This does not update the parent scope.</i>
</div>
<h2>Binding</h2>
<div>
<strong>get:</strong> {{isolatedBindingFoo}}
</div>
<h2>Expression</h2>
<div>
<input ng-model="isolatedFoo">
<button class="btn" ng-click="isolatedExpressionFoo({newFoo:isolatedFoo})">
Submit</button>
<i>// And this calls a function on the parent scope.</i>
</div>
</my-component>
I have made sample demo to illustrate the approach please check this.
I've looked all over SO and Google and seen a whole bunch of reasons why ng-show wouldn't work inside of ng-repeat (scope issues, binding issues, etc.) but I can't quite pin down why mine isn't working. I was hoping that an extra pair of eyes could help me out. I'm very new to Angular, so hopefully my code makes some sense.
The goal: when $scope.current_set changes, the visible lmf-optionset changes. Currently, only the first optionset loads via Decision_Tree:loaded, and then when I try to load the next one via clickbox:clicked, $scope.current_set changes, but the view won't update.
JS
angular.module('lmf.option_set', [])
.controller('OptionsetCtrl', ['$scope', 'Optionsets', 'Decision_Tree',
function($scope, Optionsets, Decision_Tree) {
$scope.Optionsets = Optionsets;
$scope.current_set = {
name: null
};
$scope.$on('clickbox:clicked', function() {
$scope.current_set = Decision_Tree.get_next_optionset();
});
$scope.$on('Decision_Tree:loaded', function() {
$scope.current_set = Decision_Tree.get_next_optionset();
});
}
])
HTML
<div ng-controller='OptionsetCtrl'>
<div ng-repeat='set in Optionsets.option_sets'>
<div ng-show="set.name == current_set.name" lmf-optionset="{{set.name}}"></div>
</div>
</div>
Apparently lmf-optionset creates an isolated scope for each entry. These child scopes have link to parent's current_set, but they doesn't maintain a two-way binding. So when you are replacing the current_set value in you event handlers, child scopes are unaware that current_set was changed, and they are still referring to the old value.
You can either rewrite your event handlers like this:
$scope.current_set.name = Decision_Tree.get_next_optionset().name;
Or you can use controller as construction like this:
<div ng-controller='OptionsetCtrl as vm'>
<div ng-repeat='set in Optionsets.option_sets'>
<div ng-show="set.name == vm.current_set.name" lmf-optionset="{{set.name}}"></div>
</div>
</div>