Reusing piece of HTML + knockout - javascript

Basing on nice tutorial I've created an overlay with Google-style spinner. HTML in my case is minimum and looks like following:
<div class="spinner-wrapper" data-bind="visible: spinnerVisible">
<svg class="spinner" viewBox="25 25 50 50">
<circle cx="50" cy="50" r="20" fill="none" stroke-width="4" stroke-miterlimit="10" />
</svg>
</div>
Note the data-bind="visible: spinnerVisible".
I'd like to reuse this among different places in my application to avoid DRY problems. What approach may I take? Keep in mind, that visibility of the spinner wrapper will be controlled by different viewmodels (although I may guarantee each one to have spinnerVisible property).

I'd use a (template-only) knockout component with the new web component-like syntax.
Two steps to get it to work:
1. Register a component and define its template paramaters:
Here, you define your template and the parameters it needs to render. In your case, only a named observable that controls the visible binding.
ko.components.register('spinner', {
template:
'<div data-bind="visible: spinnerVisible">A spinner<hr/></div>'
});
2. Use it with the correct parameters in your HTML
Now, whatever your viewmodel's spinner controlling property may be, you can reuse the same component to render it. For example:
<spinner params="spinnerVisible: loading"></spinner>
<spinner params="spinnerVisible: pendingRequests().length > 0"></spinner>
Note that if you use the regular component binding, you'll lose some of the clean syntax.
Here's an example showing the code above in action:
ko.components.register('spinner', {
template:
'<div data-bind="visible: visible">A spinner<hr/></div>'
});
ko.applyBindings({
someProp: ko.observable(true)
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<p>Regular syntax</p>
<div data-bind="component: {
name: 'spinner',
params: { visible: someProp }
}"></div>
<p>Web component</p>
<spinner params='visible: someProp'></spinner>
<button data-bind="click: someProp.bind(null, !someProp())">toggle</button>

Related

Vue doesnt update component on dynamic variable condition change

I am working with Vuejs. I want to render components based on value of variable val.
My component looks like this
<template v-if="this.$val===1">
<component1 />
</template>
<template v-if="this.$val===2">
<component2 />
</template>
I have defined a global variable val using Vue.prototype and I am updating it using onclick function,where I am changing value of val to 2 but after clicking it doesnt show component2 instead of component 1.
Define val globally in main.js using following line of code
Vue.prototype.$val = 1;
Can someone please help me with this. Thanks
td,dr; Vue.prototypeis not reactive.
I'm going to enumerate issues as I observe them, hoping you'll find them useful.
You're not specifying which version of Vue you're using. Since you're using Vue.prototype, I'm going to guess you're using Vue 2.
Never use this in a <template>.
Inside templates, this is implicit (sometimes formulated: "inside templates this doesn't exist"). What would be this.stuff in controller, is stuff in the template.
You can't conditionally swap the top level <template> of a Vue component. You need to take the conditional either one level up or one level down:
one level up would be: you create separate components, one for each template; declare them and have the v-if in their parent component, rendering one, or the other
one level down would be: you move the v-if inside the top level <template> tag of the component. Example:
<template><!-- top level can't have `v-if` -->
<div v-if="val === 1">
val is 1
<input v-model="val">
</div>
<div v-else>
val is not 1
<input v-model="val">
</div>
</template>
<script>
export default {
data: () => ({ val: 1 })
}
</script>
Note <template> tags don't render an actual tag. They're just virtual containers which help you logically organise/group their contents, but what gets rendered is only their contents.1 So I could have written the above as:
<template><!-- top level can't have v-if -->
<template v-if="val === 1">
<div>
val is 1
<input v-model="val">
</div>
</template>
<template v-else>
<template>
<template>
<div>
val is not 1
<input v-model="val">
</div>
</template>
</template>
</template>
</template>
And get the exact same DOM output.
For obvious reasons, <template> tags become useful when you're working with HTML structures needing to meet particular parent/child constraints (e.g: ul + li, tr + td, tbody + tr, etc...).
They're also useful when combining v-if with v-for, since you can't place both on a single element (Vue needs to know which structural directive has priority, since applying them in different order could produce different results).
Working example with what you're trying to achieve:
Vue.prototype.$state = Vue.observable({ foo: true })
Vue.component('component_1', {
template: `
<div>
This is <code>component_1</code>.
<pre v-text="$state"/>
<button #click="$state.foo = false">Switch</button>
</div>
`})
Vue.component('component_2', {
template: `
<div>
This is <code>component_2</code>.
<pre v-text="$state"/>
<button #click="$state.foo = true">Switch back</button>
</div>
`})
new Vue({
el: '#app'
})
<script src="https://unpkg.com/vue#2.7.10/dist/vue.min.js"></script>
<div id="app">
<component_1 v-if="$state.foo"></component_1>
<component_2 v-else></component_2>
</div>
Notes:
<div id="app">...</div> acts as <template> for the app instance (which is, also, a Vue component)
Technically, I could have written that template as:
<div id="app">
<template v-if="$state.foo">
<component_1 />
</template>
<template v-else>
<component_2 />
</template>
</div>
, which is pretty close to what you were trying. But it would be slightly more verbose than what I used, without any benefit.
I'm using a Vue.observable()2 for $state because you can't re-assign a Vue global. I mean, you can, but the change will only affect Vue instances created after the change, not the ones already created (including current one). In other words, Vue.prototype is not reactive. This, most likely, answers your question.
To get past the problem, I placed a reactive object on Vue.prototype, which can be updated without being replaced: $state.
1 - there might be an exception to this rule: when you place text nodes inside a <template>, a <div> wrapper might be created to hold the text node(s). This behaviour might not be consistent across Vue versions.
2 - Vue.Observable() was added in 2.6.0. It's a stand-alone export of Vue's reactivity module (like a component's data(), but without the component). In v3.x Vue.Observable() was renamed Vue.reactive(), to avoid confusion/conflation with rxjs's Observable.
global variables are accessed in template without this keyword which means $val===1 will work.
Solution 1:
<template>
<component1 v-if='$val === 1' />
<component2 v-else/>
</template>
This will work.
But you could make use of dynamic components in your case.
Solution 2:
<template>
<component :is='currentComponent'/>
</template>
<script>
\\imports of your components will go here
export default {
name: 'app',
components: {
component1, component2
},
computed:{
currentComponent(){
return this.$val === 1?component1:component2;
}
}
}
</script>
Dynamic components are more performant and helps you maintain state of component.

Vue 3 Add Wrapper Around Each Child

I have a form component as the following:
<form-component>
<text-component name="test1" />
<select-component name="test2" />
</form-component>
I need FormComponent to be able to apply a wrapper div around each child
From the above code, FormComponent output should be something like this:
<form>
<div class="mb-3">
<text-component name="test1" />
</div>
<div class="mb-3">
<select-component name="test2" />
</div>
</form>
Here's one way to go about it:
const formChildren = [{
name: 'test1',
is: TextComponent
}, {
name: 'test2',
is: SelectComponent
}]
<form-component :children="formChildren" />
FormComponent.vue
<template>
<form>
<div
v-for="(child, index) in children"
:key="index"
class="mb-3"
>
<component v-bind="child" />
</div>
</form>
</template>
<script setup>
defineProps(['children'])
</script>
And here's a working demo of the suggestion you made in the comments, to loop through contents of $slots.default().
If you prefer writing your logic in template syntax, that's the way to go, I see nothing wrong with it.
I personally prefer the first approach, as my tendency is (in general), to limit the template syntax to minimum. Keeping components in object or map structures allows me to have granular control and automate tasks such as:
validation
event management
applying dynamic default values coming from configuration objects
handling browser/device quirks
My preference probably comes from having worked a lot in config driven environments, where the business logic is typically stored in objects. There's nothing wrong with writing it in template syntax, though but, overall, I find it limiting.

How do I make SVG components?

I try to make a set of self contained SVG components which include CSS and Javascript, that can be added to a web page via templating. The idea is to be able to add self contained components without changing the base web page. A component could be something like like:
<div id="component-1" ...>
<svg>
<css>
.. css for styling component
</css>
<line ...>
<text ....>
... component svg
<script>
.... functions that manipulate the component
</script>
</svg>
</div>
The same component may be added multiple times. Is there a standard way to do this?

KnockoutJS - binding child component's click event in parent's markup

I am trying to write a reusable component library, and have come up with this issue which is bugging me for a time now.
I have overriden the template engine using the example that is provided. I think my problem is mentioned in his 'A quick note about rewriting templates' paragraph (I am not sure though, my problem might be different). But I don't have any idea how to implement it. Any help is appreciated. Here's the problem:
My template engine basically registers templates given as 'templateName' and 'markUp'. Firstly, I have a product model, which goes as:
var Product = function(img, brand, description){
this.img = img;
this.brand = brand;
this.description = description;
};
Then, I have my parent view as:
{
name: 'productlist-view',
template: '<productlist-view class="product-list">\
<ul data-bind="foreach: {data: productlist, as: \'product\'}">\
<product-box params="parent: $parent, productInfo: product" data-bind="click: getProduct"></product-box>\
</ul>\
</productlist-view>'
}
Now in the viewModel of this productlist-view view, productlist property is defined as an array of Product instances. The product-box component is supposed to create a viewModel and an associated template for each of these Products. The product-box component is registered to knockout using:
ko.components.register('product-box', {
'viewModel': function(params){
this.product = params.product;
this.getProduct = function(){
//Will perform an ajax call here
};
},
'template': '<div class="product-box" data-bind="click: getProduct">\
<img class="product-img fl" data-bind="attr: {src: product.img}"/>\
<p data-bind="text: product.brand"></p>\
<p data-bind="text: product.description"></p>\
</div>'
})
I know, in the code above there are two bindings for the getProduct method. I will come to that. For now, just imagine the one in the productlist-view was not there.
The code above generates an html that goes like:
<productlist-view ....
<ul ....
<product-box ....
<div class="product-box" ....
<img .../>
<p ... />
<p ... />
</div>
</product-box>
<product-box ..... goes on
</ul>
</productlist-view>
In the above-code, the wrapper div in the product-box element is totally unnecessary since it just wraps the element. Moreover, the element is already wrapped within product-box element. So I want to remove the wrapper div. The problem here is that, I need the whole product visual to be clickable, but I cannot bind the click event to the getProduct method from the productlist-view template. When the foreach loop iterates in productlist-view template, the $data points to model of the product, not the viewModel (aka product-box).
How can I set this getProduct method, from the parent view?
Is there anyway to remove that unnecessary wrapper div in product-box?
In other words how can I have a clickable product-box component which goes like:
<productlist-view ....
<ul ....
<product-box data-bind="click: getProduct"....
<img .../>
<p ... />
<p ... />
</product-box>
...
</ul>
</productlist-view>
You can make a custom binding handler that would attach a click binding to the parent, but that strikes me as too clever by half (and violates encapsulation). If your click binding is associated with your component, it makes sense that the div be part of the component.
Instead of using custom product-box tags, you can use a virtual tag and the component binding, so you don't have an extraneous wrapper.

AngularJS directive with replacing

I've developed AngularJS directives to add rectangle into SVG document:
<svg version="1.1" width="200" height="100">
<g>
<!-- Works -->
<g dir-rect1="" viz-settings="settings.rect1" />
<!-- Doesn't work -->
<g dir-rect2="" viz-settings="settings.rect2" />
</g>
The only difference is between the directives is that the dir-rect1 directive doesn't use replace and the dir-rect2 directive use:
app.directive('dirRect2', function($compile) {
return {
restrict: 'AE',
scope: {
vizSettings: '=',
},
replace:true, // <--- DOESN'T WORK
template: '<rect fill="{{vizSettings.fill}}" stroke="{{vizSettings.fill}}" width="{{vizSettings.width}}" height="{{vizSettings.height}}" />',
link: function($scope, elem, attr, ctrl) {
console.debug($scope);
}
};
});
Why when I use replacing in my dirRect2 directive, I cannot see a rect?
It seems that generated code is right in both cases.
You can see example on plunker.
Two things:
replace is deprecated so, I don't recommend you to use it
replace copy the attributes to the children, and that is your problem.
The second directive generates this:
<rect fill="#111111" stroke="#111111" width="10" height="20" dir-rect2="" viz-settings="settings.rect2" class="ng-isolate-scope"></rect>
And it seems that dir-rect2 and viz-settings is breaking the svg in some way... if you remove this attributes with the same code... svg seems to work:
<rect fill="#111111" stroke="#111111" width="10" height="20" ></rect>
in dirRect1 replace is false (default value) and in dirRect2 is true, so you see a different behavior between the two directives.
The rectangle is not displayed since "rect" tag is required to render the shape in svg! (as I just realized since I never played with svg)
(when you define "replace: true" you are telling Angular to remove the original tag on which the directive is applied)

Categories