Binding '&' method in a routed component - javascript

Consider this simplified Angular 1.5.x component (all in jsfiddle):
appModule.component('mainComponent', {
controller: function() {
var x = 0;
this.broadcast = function() {
this.onUpdate({
count: x++
});
};
this.broadcast();
},
bindings: {
onUpdate: '&'
},
template: '<input type="button" ng-click="$ctrl.broadcast()" value="add"/>'
});
Html (in body)
<main-component on-update="this.count = count"></main-component>
Value in parent: {{count}}
When clicking the component button, the count variable is being update ('&' onUpdate is binded well).
Now I would like to have a route to the component from ui.router:
$stateProvider.state({
name: 'state1',
component: 'mainComponent',
url: "#"
});
Navigating to the state, results in Cannot read property '2' of null, removing the onUpdate member fix the error but break the binding.
What am I doing wrong? What is the way to bind callback methods of components when using ui.router route to components.
jsfiddle

it looks like binding callbacks using "resolve" is not currently supported, see here: https://github.com/angular-ui/ui-router/issues/2793

Related

$onChanges not triggered

Get updated by model but don't update the model. Should I use the $onChanges function
I have a model:
class Model {
constructor(data) {
this.data = data;
}
getData() {
return this;
}
}
2 nested components:
var parentComponent = {
bindings: {
vm: '<'
},
controller: function() {
var ctrl = this;
},
template: `
<div>
<a ui-sref="hello.about" ui-sref-active="active">sub-view</a>
Parent component<input ng-model="$ctrl.vm.data">
<ui-view></ui-view>
</div>
`
};
var childComponent = {
bindings: {
vm: '<'
},
template: `
<div>
Child component <input ng-model="$ctrl.vm.data">
<br/>
Child component copy<input ng-model="$ctrl.vmCopy.data">
<br/>
Child component doCheck<input ng-model="$ctrl.vmCheck.data">
</div>
`,
controller: function() {
var ctrl = this;
ctrl.$onChanges = function(changes) {
ctrl.vmCopy = angular.copy(ctrl.vm);
ctrl.vm = ctrl.vm;
};
ctrl.$doCheck = function () {
var oldVm;
if (!angular.equals(oldVm, ctrl.vm)) {
oldVm = angular.copy(ctrl.vm);
ctrl.vmCheck = oldVm;
console.log(ctrl)
}
}
}
}
Both get data from resolve:
.config(function($stateProvider) {
var helloState = {
name: 'hello',
url: '/hello',
resolve: {
vm: [function() {
return myModel.getData();
}]
},
component: 'parent'
}
var aboutState = {
name: 'hello.about',
url: '/about',
resolve: {
vm: [function() {
return myModel.getData();
}]
},
component: 'child'
}
$stateProvider.state(helloState);
$stateProvider.state(aboutState);
})
I would like my components to be updated when model change or when parent change, but I don't wan't them to update the model.
I thought that was why one way binding '<' stands for.
Here's a fiddle to illustrate what I want to do.
In other word:
I would like the child component to be updated on parent changes but don't want the child to update the parent.
You can see in the fiddle that if I bind directly to local scope, child get update from parent but also update the parent
If I copy the binding to local scope, child isn't updating the parent but also doesn't get updated by parent.
If
With object content — Use the $doCheck Life-cycle Hook1
When binding an object or array reference, the $onChanges hook only executes when the value of the reference changes. To check for changes to the contents of the object or array, use the $doCheck life-cycle hook:
app.component('nvPersonalTodo', {
bindings: {
todos: "<"
},
controller: function(){
var vm = this;
this.$doCheck = function () {
var oldTodos;
if (!angular.equals(oldTodos, vm.todos)) {
oldTodos = angular.copy(vm.todos);
console.log("new content");
//more code here
};
}
})
From the Docs:
The controller can provide the following methods that act as life-cycle hooks:
$doCheck() - Called on each turn of the digest cycle. Provides an opportunity to detect and act on changes. Any actions that you wish to take in response to the changes that you detect must be invoked from this hook; implementing this has no effect on when $onChanges is called. For example, this hook could be useful if you wish to perform a deep equality check, or to check a Date object, changes to which would not be detected by Angular's change detector and thus not trigger $onChanges. This hook is invoked with no arguments; if detecting changes, you must store the previous value(s) for comparison to the current values.
— AngularJS Comprehensive Directive API Reference -- Life-cycle hooks
For more information,
AngularJS angular.equals API Reference
AngularJS 1.5+ Components do not support Watchers, what is the work around?

Parent's data change does not update child component in vuejs

I have the following:
Vue.component('times-updated', {
template: '<span>Times Updated: {{ timesUpdated }}</span>',
data: function() {
return {
timesUpdated: this.$parent.myData.timesUpdated
}
}
});
var vm = new Vue({
el: '#test',
data: function() {
return {
myData: {}
}
}
})
setInterval(function(){
$.ajax({
url: `${window.location.href}/json`, // This just returns an array : array.timesUpdated: 2 etc
}).done(function (data) {
vm.myData = data; // changes this data
});
}, 1000)
and am using the following html:
<div class="test">
<times-updated></times-updated>
</div>
I poll a REST API that returns an array which includes a timesUpdated property:
{
timesUpdated: 5
}
My intention is that every second I use jQuery's $.ajax method to call the API, update the myData data object on vm, which would then update the times-updated component.
The code works on initial page load, the times-updated component can retrieve the value on its parent's myData property, but whilst I have confirms that vm.myData does reflect the new value from the API, the component doesn't update its display to show the new count.
What am i doing wrong?
The data function is only called once during the life cycle of the component; when it is initially created. So essentially your component is just displaying the value as it existed when the component was created.
Additionally, it's generally bad practice to reach out of a component to get a data value. Vue is props down, events up. You should convert your component to use a property.
Vue.component('times-updated', {
props:["times"],
template: '<span>Times Updated: {{ times }}</span>',
})
The fact that you are using a function to define the Vue in this particular case doesn't really matter, it's just not a typical practice. Components require a function because they need an isolated scope.
Here is an example.
That callback is required only in components
// vue instance
new Vue({
data: {
status: true
}
};
// vue components (callback)
Vue.component('custom-component', {
data: function() {
return {
status: false
}
}
});

How to call a vue.js function on page load

I have a function that helps filter data. I am using v-on:change when a user changes the selection but I also need the function to be called even before the user selects the data. I have done the same with AngularJS previously using ng-init but I understand that there is no such a directive in vue.js
This is my function:
getUnits: function () {
var input = {block: this.block, floor: this.floor, unit_type: this.unit_type, status: this.status};
this.$http.post('/admin/units', input).then(function (response) {
console.log(response.data);
this.units = response.data;
}, function (response) {
console.log(response)
});
}
In the blade file I use blade forms to perform the filters:
<div class="large-2 columns">
{!! Form::select('floor', $floors,null, ['class'=>'form-control', 'placeholder'=>'All Floors', 'v-model'=>'floor', 'v-on:change'=>'getUnits()' ]) !!}
</div>
<div class="large-3 columns">
{!! Form::select('unit_type', $unit_types,null, ['class'=>'form-control', 'placeholder'=>'All Unit Types', 'v-model'=>'unit_type', 'v-on:change'=>'getUnits()' ]) !!}
</div>
This works fine when I select a specific item. Then if I click on all lets say all floors, it works. What I need is when the page is loaded, it calls the getUnits method which will perform the $http.post with empty input. In the backend I have handled the request in a way that if the input is empty it will give all the data.
How can I do this in vuejs2?
My Code: http://jsfiddle.net/q83bnLrx
You can call this function in the beforeMount section of a Vue component: like following:
// .....
methods: {
getUnits: function() { /* ... */ }
},
beforeMount() {
this.getUnits()
},
// ......
Working fiddle: https://jsfiddle.net/q83bnLrx/1/
There are different lifecycle hooks Vue provide:
I have listed few are :
beforeCreate: Called synchronously after the instance has just been initialized, before data observation and event/watcher setup.
created: Called synchronously after the instance is created. At this stage, the instance has finished processing the options which means the following have been set up: data observation, computed properties, methods, watch/event callbacks. However, the mounting phase has not been started, and the $el property will not be available yet.
beforeMount: Called right before the mounting begins: the render function is about to be called for the first time.
mounted: Called after the instance has just been mounted where el is replaced by the newly created vm.$el.
beforeUpdate: Called when the data changes, before the virtual DOM is re-rendered and patched.
updated: Called after a data change causes the virtual DOM to be re-rendered and patched.
You can have a look at complete list here.
You can choose which hook is most suitable to you and hook it to call you function like the sample code provided above.
You need to do something like this (If you want to call the method on page load):
new Vue({
// ...
methods:{
getUnits: function() {...}
},
created: function(){
this.getUnits()
}
});
you can also do this using mounted
https://v2.vuejs.org/v2/guide/migration.html#ready-replaced
....
methods:{
getUnits: function() {...}
},
mounted: function(){
this.$nextTick(this.getUnits)
}
....
Beware that when the mounted event is fired on a component, not all Vue components are replaced yet, so the DOM may not be final yet.
To really simulate the DOM onload event, i.e. to fire after the DOM is ready but before the page is drawn, use vm.$nextTick from inside mounted:
mounted: function () {
this.$nextTick(function () {
// Will be executed when the DOM is ready
})
}
If you get data in array you can do like below. It's worked for me
<template>
{{ id }}
</template>
<script>
import axios from "axios";
export default {
name: 'HelloWorld',
data () {
return {
id: "",
}
},
mounted() {
axios({ method: "GET", "url": "https://localhost:42/api/getdata" }).then(result => {
console.log(result.data[0].LoginId);
this.id = result.data[0].LoginId;
}, error => {
console.error(error);
});
},
</script>
methods: {
methodName() {
fetch("url").then(async(response) => {
if (response.status === 200) {
const data = await response.json();
this.xy = data.data;
console.log("Success load");
}
})
}
}
you can do it using created() method. it will fire once page fully loaded.
created:function(){
this.fillEditForm();
},

Configure ui-router with components containing multiple bindings

I am trying to find a better solution to use the ui-router together with angular components.
Consider two simple components:
app.component('componentOne', {
template: '<h1>My name is {{$ctrl.name}}, I am {{$ctrl.age}} years old.</h1>',
bindings : {
name : '#',
age : '#'
}
}
);
app.component('componentTwo', {
template: '<h1>I am component 2</h1>'
});
Right now, I am specifying the component and its parameter using the template property:
app.config(function($stateProvider, $urlRouterProvider ){
$stateProvider
.state('component1', {
url: "/component1",
template: "<component-one name=\"foo\" age=\"40\"></component-one>"
})
.state('component2', {
url: "/component2",
template: "<component-two></component-two>"
})
});
While this is working fine, I have components with arround ten bindings which makes the configuration of the ui-router realy awkward.
I tried using the component property but this doesn't work for me at all. The only other solution I found is to specify the parent using the require property and omit the bindings - but this doesn't feel right for me... Is there a better way to do this?
Here is a plnkr.
UI-Router component: routing exists in UI-Router 1.0+ (currently at 1.0.0-beta.1)
Here's an updated plunker: https://plnkr.co/edit/VwhnAvE7uNnvCkrrZ72z?p=preview
Bind static values
To bind static data to a component, use component and a resolve block which returns static data.
$stateProvider.state('component1', {
url: "/component1",
component: 'componentOne',
resolve: { name: () => 'foo', age: () => 40 }
})
Bind async values
To bind async values, use a resolve which returns promises for data. Note that one resolve can depend on a different resolve:
$stateProvider.state('component1Async', {
url: "/component1Async",
component: "componentOne",
resolve: {
data: ($http) => $http.get('asyncFooData.json').then(resp => resp.data),
name: (data) => data.name,
age: (data) => data.age
}
});
Bind lots of values
You mention you have 10 bindings on a component. Depending on the structure of the data you're binding, you can use JavaScript to construct the resolve block (it's "just javascript" after all)
var component2State = {
name: 'component2',
url: '/component2',
component: 'componentTwo',
resolve: {
data: ($http) => $http.get('asyncBarData.json').then(resp => resp.data)
}
}
function addResolve(key) {
component2State.resolve[key] = ((data) => data[key]);
}
['foo', 'bar', 'baz', 'qux', 'quux'].forEach(addResolve);
$stateProvider.state(component2State);
Alternatively, you can move your bindings a level down and create an object which will be the only bindings. If 10 bindings is what is bothering you.
One alternative you can try is to override the template by custom properties of states in $stateChangeStart event.
Run block like this to achieve this kind of behaviour.
app.run(function($rootScope){
//listen to $stateChangeStart
$rootScope.$on("$stateChangeStart",function(event, toState, toParams, fromState, fromParams, options){
//if the component attribute is set, override the template
if(toState.component){
//create element based on the component name
var ele = angular.element(document.createElement(camelCaseToDash(toState.component)));
//if there is any binding, add them to the element's attributes.
if(toState.componentBindings){
angular.forEach(toState.componentBindings,function(value,key){
ele.attr(key,value)
})
}
//you may also do something like getting bindings from toParams here
//override the template of state
toState.template = ele[0].outerHTML;
}
})
//convert camel case string to dash case
function camelCaseToDash(name) {
return name.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
}
})
And with this now you can have component property in your state config.
app.config(function($stateProvider, $urlRouterProvider ){
$stateProvider
.state('component1', {
url: "/component1",
component:"componentOne",
componentBindings:{
name:"foo",
age:"40",
}
})
.state('component2', {
url: "/component2",
component:"componentTwo",
})
});
Here is the working plunker.
Still you may have a large config function, but it will look not so awkward.

Angular 1.5 component router sibling components

Is there a way with the new component router for Angular 1.5 to keep the sibling component rendered in the ng-outlet directive?
I want to show the Detail View in parallel with the sibling List View.
As far as I understand the official Docs (https://docs.angularjs.org/guide/component-router) it should be possible with the $$router and bind it to the child component.
Here is what I tried to do:
http://plnkr.co/edit/KzW8fLAxrte9jSg5jhEg?p=preview
<ng-outlet><crisis-detail $router="$$router"></crisis-detail>
There i a similiar post on this binding topic:
Angular 1.5 component $router binding
There is no ability to show siblings simultaneously with Angular 1.5 Component Router as far as I know.
However, workaround is to make sibling to be actually child, and then use empty component to show with default, "no details" route.
Workaround:
First, we need some root component to activate list itself:
.component('listRoot', {
template: '<ng-outlet></ng-outlet>', //just ng-outlet, to render List inside
$routeConfig: [
{path: '/...', name: 'ListRoot',component: 'list' },
]
})
Then we need to add components for list, detail, and noDetail mode.
.component('list', {
template: 'List ... <ng-outlet></ng-outlet>',
$routeConfig: [
{path: '/', name: 'List',component: 'noDetails', useAsDefault: true },
{path: '/:id',name: 'Details',component: 'details'}
],
bindings: {
$router: '<'
},
controller: function () {
var ctrl = this
$routerOnActivate = function(route) {
ctrl.router = this.$router;
}
this.goToDetails = function(id) {
ctrl.$router.navigate(['Details', {id: id}])
}
}
})
.component('detail', {
template: 'Details: <a ng-link="[\'List\']">Go Back</a>'
})
.component('noDetails', {
template: '' //just empty template
})
Accessing parent:
Also, to be able to notify parent (in your example - sibling Detail component telling it ID to List, and List marking it as selected after) you can use require component option, to be able to access parent component scope.
.component('detail', {
template: 'Details: <a ng-link="[\'List\']">Go Back</a>',
require: {
parent: '^list'
},
controller: {
this.goBackWithNotify = function(data) {
ctrl.parent.someParentComponentProperty = data;
}
}
})
Edited plunker with example.
PS: I used more recent version of router.

Categories