Svelte if block's behavior is different form component's property when is set to undefined - undefined

I prefer to define a visible property for Svelte components rather than writing Svelte's if block in template. In my opinion, it is more clean to set a visible property instead of adding 2 lines of if block codes in the template.
I am trying to define a visible property for a Svelte component which must have the same behavior of if block.
Here is the repl.
El.svelte
<script>
export let visible = true // should be visible by default
</script>
{#if visible}
<svelte:element this="div" {...$$restProps}>
<slot />
</svelte:element>
{/if}
App.svelte
<script>
import El from './El.svelte';
let visible = undefined
</script>
<El {visible}>Component (undefined)</El>
{#if visible}
Svelte if block (undefined)
{/if}
The if block is working as expected, but the component is rendered which shouldn't be.
It seems that undefined property isn't passed to component during Svelte compilation.
What is the best way to make consistent behavior for visible property?
Update:
The reason I want to use as visible property is the final code is more readable. Here is a sample code using if block:
{#if invoice.payment}
{#if invoice.payment?.approved}
<Badge>Approved</Badge>
{#if !order.confirmed}
<Button>Void Payment</Button>
{/if}
{/if}
{#if invoice.payment?.status }
<b>{invoice.payment.tranasction.id}</b>
{/if}
{/if}
And using visible property:
<Div visible={invoice.payment?.approved}>
<Badge>Approved</Badge>
<Button visible={!order.confirmed}>Void Payment</Button>
</Div>
<B visible={invoice.payment?.status}>{invoice.payment.tranasction.id}</B>
The updated repl to support all scenarios.
But I'm not sure if it's correct implementation or future usage in nested components and event/property/action bindings.
Update 2:
I don't think this is a my-opinion-based usage: Vue.js has v-if and
Angular has ng-if that does the same thing as visible.

If you really want to stick with this approach (which I would not recommend - just use {#if} when you need it), then the best way to deal with this might be to not allow undefined to ever be passed as a value, because that resolves to the default set in the component (in this case visible = true).
So if you have a value that is potentially undefined, make sure it resolves to either true or false, e.g.
<El visible={visible ?? false}>
Edit in response to question update: Using property inspection is indeed a working approach, though I would not allow arbitrary values, only boolean, null or undefined. The simplest setup for this would be:
export let visible = undefined;
if ('visible' in $$props == false)
visible = true;
A reactive statement is not required in this case, because whether a property is defined at all does not change. (This requires setting visible={false} instead of visible="false".)
One reason to not do this at all is the use of $$restProps, as the docs state:
It shares the same optimisation problems as $$props, and is likewise not recommended.
Also, you will run into issues if you want to do event handling.

Related

Angular4 ng-content gets built when ngIf is false

I have a problem with the new ng-content transclusion.
Let's say I have a component my-component that, in its ngOnInit() function does some heavy operation on load (for now, just a console.log()).
I have a wrapper, that displays the content via transclusion (my-wrapper.component.html).
<ng-content></ng-content>
If I set the surroundings up like this, the log statement doesn't show:
<my-wrapper *ngIf="false">
<my-component></my-component>
</my-wrapper>
I assume, the my-wrapper component does not get built, so the content is ignored.
But if I try to move the logic into the my-wrapper component like this (my-wrapper.component.html):
<ng-container *ngIf="false">
<ng-content></ng-content>
</ng-container>
I always see the console.log() output. I guess, the my-component gets built and then stored away until the *ngIf becomes true inside my-wrapper.
The intention was to build a generic "list-item + detail" component. Say I have a list of N overview-elements (my-wrapper), that get rendered in a *ngFor loop. Every of those elements has its own detail component (my-component) that is supposed to load its own data, once I decide to show more infos to a specific item.
overview.html:
<ng-container *ngFor="let item of items">
<my-wrapper>
<my-component id="item.id"></my-component>
</my-wrapper>
</ng-container>
my-wrapper.component.html:
<div (click)="toggleDetail()">Click for more</div>
<div *ngIf="showDetail">
<ng-content></ng-content>
</div>
Is there a way to tell Angular, to ignore the transcluded content until it is necessary to be added to the page? Analogously to how it was in AngularJS.
Based on the comment of #nsinreal I found an answer. I find it to be a bit abstruse, so I'm trying to post it here:
The answer is to work with ng-template and *ngTemplateOutlet.
In the my-wrapper component, set up the template like this (my-wrapper.component.html):
<div (click)="toggleDetail()">Click for more</div>
<div *ngIf="showDetail" [hidden]="!isInitialized">
<ng-container *ngTemplateOutlet="detailRef"></ng-container>
</div>
Note, that the [hidden] there is not really necessary, it hides the "raw" template of the child until it decides it is done loading. Just make sure, not to put it in a *ngIf, otherwise the *ngTemplateOutlet will never get triggered, leading to nothing happening at all.
To set the detailRef, put this in the component code (my-wrapper.component.ts):
import { ContentChild, TemplateRef } from '#angular/core';
#Component({ ... })
export class MyWrapperComponent {
#ContentChild(TemplateRef) detailRef;
...
}
Now, you can use the wrapper like this:
<my-wrapper>
<ng-template>
<my-component></my-component>
</ng-template>
</my-wrapper>
I am not sure, why it needs such complicated "workarounds", when it used to be so easy to do this in AngularJS.
By doing this:
<my-wrapper *ngIf="false">
<my-component></my-component>
</my-wrapper>
You are not calling MyComponent component, because the *ngIf is false. that means, that not calling it you are not instancing it and, therefore, not passing through its ngOnInit. And that's why you are not getting the console log.
By doing this:
<ng-container *ngIf="false">
<ng-content></ng-content>
</ng-container>
You are inside the component, you are just limiting what to render in your template, but you already instanced your component and, therefore, you passed through your ngOnInit and you get your console log done.
If, you want to limit something (component call with selector or a ng-content or even a div) until you have some data available, you can do the following:
datasLoaded: Promise<boolean>;
this.getData().subscribe(
(data) => {
this.datasLoaded = Promise.resolve(true); // Setting the Promise as resolved after I have the needed data
}
);
And in your template:
<ng-container *ngIf="datasLoaded | async">
// stuff here
</ng-container>
Or:
<my-component *ngIf="datasLoaded | async">
// Didn't test this one, but should follow the same logic. If it doesn't, wrap it and add the ngIf to the wrapper
</my-component>
It’s because Ng content happens at the build time and when you pass the content it is actually not removed or recreated with the ngIf directive. It is only moved and the component is instantiated .
I encountered this problem recently as well but settled on a different solution than the currently accepted one.
Solution (TL;DR)
(Solution is for AngularDart; I figure it's similar in Angular though)
Use a structural directive; tutorials linked below.
Instead of:
<my-wrapper>
<my-contents></my-contents>
</my-wrapper>
your usage becomes:
<div *myWrapper>
<my-contents></my-contents>
</div>
which is shorthand for the following (in AngularDart; I think Angular uses <ng-template>)
<template myWrapper>
<div>
<my-contents></my-contents>
</div>
</template>
The MyWrapper directive logic is similar to NgIf except it has its own logic to compute the condition. Both of the following tutorials explain how to create an NgIf-like directive and how to pass it your own inputs using the special microsyntax (e.g. *myWrapper="myInput: expression"). Note that the microsyntax doesn't support outputs (#Output), but you can mimic an output by using an input that is a function.
Tutorial for Angular
Tutorial for AngularDart
Caveat: Since this is just a directive, it shouldn't do anything more complicated than instantiating a template ref at the appropriate time and maybe specifying some DI providers. For example, I would avoid trying to apply styles or instantiating a complex tree of components in the directive. If I wanted to create a list component, I would probably take the #ContentChild(TemplateRef) approach described in another answer; you would lose the asterisk shorthand for creating <template> but you would gain the full power of components.
My problem
My team owns an app that's part of a larger web application with other apps owned by other teams. Our components assume they can inject a MyAppConfiguration object, but this object can only be injected after it is loaded with an asynchronous request. In our app this is not a problem: we have a "shell" component that hides everything behind an ngIf until the configuration is loaded.
The problem is when other teams want to reference our components. We don't want them to duplicate the "wait until configuration is loaded" logic every time, so I tried creating a wrapper component that can be used like so:
<my-app-wrapper>
<my-app-component></my-app-component>
</my-app-wrapper>
The wrapper injects a service object and hides its contents behind an ngIf until the service says that the configuration is loaded.
Like the question poster, I discovered that the ng-content approach doesn't work as intended: while the contents are correctly hidden from the DOM, Angular still instantiates the components causing dependency injection to fail.
The solution that I settled on was to rewrite the wrapper component as a structural directive.

Angular2 function call from html element with no load event (or similiar)

I am new to Angular and have run into a problem that seems to have a javascript work around but they aren't very elegant.
I have a model with an array property. I ngfor the list property to build some html selection options. This is all working nicely. The problem comes when I am trying to set default value...the html elements don't have a load event.
I tried numerous html elements and they don't appear to have a load event either but I certainly could be doing it wrong.
I have seen a solution to put javascript tag right after the html and I could do that but I was really looking for a more elegant way in Angular.
I saw this SO post and thought that was my answer but there is a warning given that I agree with and thus it doesn't appear to be a good solution.
Regardless I tried it just to see if it would work and I got:
Failed to execute 'setAttribute' on 'Element': '{{loadDefaults()}}' is not a valid attribute name
<span {{loadDefaults()}} ></span>
So how can I fire an AS2 function in the component to load the default values?
HTML (btw this is NOT a full page load so there is no body tag):
<tr>
<td *ngFor="let loc of locOptions;">
<span>{{loc.text}}</span>
<input type="radio" name="radiogroup" [value]="loc.value" (change)="onSelectionChange(loc.value)">
</td>
</tr>
Edit
I thought perhaps mistakenly that ngoninit would fire too soon...before the html elements are rendered.
So perhaps what is being suggested is that I add a boolean is default to the model and bind THAT as the element is rendered.
In your ngonit function set this.locOptions to your default values. The value can be changed later on in any function and the change will be reflected in the view. Hope this helps you.
You should use ngOnInit to init you data, and call retrieve your data from your component :
defaults : any;
ngOnInit {
this.defaults = loadDefaults();
}
loadDefaults() {
//get data
}
HTML :
<span>{{defaults}}</span>

Why do Polymer's computed properties need explicit property arguments?

I was digging a bit into the Polymer 1.0 elements and I am a little curious about the computed properties.
For example, in paper-drawer-panel.html,
<dom-module id="paper-drawer-panel" …>
…
<div id="main" style$="[[_computeDrawerStyle(drawerWidth)]]">
…
</div>
…
</dom-module>
<script>
Polymer({
is: 'paper-drawer-panel',
…
_computeDrawerStyle: function(drawerWidth) {
return 'width:' + drawerWidth + ';';
},
…
</script>
drawerWidth is a property of paper-drawer-panel, so why is it so important to explicitly include it in the parameters of the computed property?
Is
[[_computeDrawerStyle()]]
…
_computeDrawerStyle: function () {
return 'width:' + this.drawerWidth + ';';
}
Is this bad practice?
Explicit arguments in computed bindings serve an important purpose: telling Polymer which properties the computed binding depends on. This allows Polymer to know when to recalculate and update the computed binding.
Take [[_computeDrawerStyle()]], for example. In this case, Polymer has no idea what other properties the computed binding depends on, and will only calculate it once on load.
As soon as you add drawerWidth explicitly ([[_computeDrawerStyle(drawerWidth)]]) Polymer now knows that it should run the computed binding again for a new value every time drawerWidth changes.
I think you are confused. What you are referring to in your code example here style$="[[_computeDrawerStyle(drawerWidth)]]" is a call to a private function called _computeDrawerStyle and of course you need to explicitly give it the right parameters. Check the documentation here to learn about computed properties.
Polymer has two separate concepts and you are confusing them.
Computed properties. These are properties that depend on other ones and are recalculated whenever their components changed. You can then databind the value of that computed property as a property value. <paper-draw-panel> does NOT have a computed property (I checked the code).
Function calls referenced in the databinding (which is what _computeDrawStyle) is. This causes Polymer to call that function (of the element) when ever any of its parameters changed. The parameters are all properties (or you can use subproperties of objects and indexes of arrays) This is what is happening here.

How to access more than 2 DOM elements "The AngularJS way"?

I'm starting to learn angularJS better, and I've noticed that AngularJS tries to make strong emphasis on separating the view from the controller and encapsulation. One example of this is people telling me DOM manipulation should go in directives. I kinda got the hang of it now, and how using link functions that inject the current element allow for great behavior functionality, but this doesn't explain a problem I always encounter.
Example:
I have a sidebar I want to open by clicking a button. There is no way to do this in button's directive link function without using a hard-coded javascript/jquery selector to grab the sidebar, something I've seen very frowned upon in angularJS (hard-coding dom selectors) since it breaks separation of concerns. I guess one way of getting around this is making each element I wish to manipulate an attribute directive and on it's link function, saving a reference it's element property into a dom-factory so that whenever a directive needs to access an element other than itself, it can call the dom-factory which returns the element, even if it knows nothing where it came from. But is this the "Angular way"?
I say this because in my current project I'm using hard-coded selectors which are already a pain to mantain because I'm constantly changing my css. There must be a better way to access multiple DOM elements. Any ideas?
There are a number of ways to approach this.
One approach, is to create a create a sidebar directive that responds to "well-defined" broadcasted messages to open/close the sidebar.
.directive("sidebar", function(){
return {
templateUrl: "sidebar.template.html",
link: function(scope, element){
scope.$root.$on("openSidebar", function(){
// whatever you do to actually show the sidebar DOM content
// e.x. element.show();
});
}
}
});
Then, a button could invoke a function in some controller to open a sidebar:
$scope.openSidebar = function(){
$scope.$root.$emit("openSidebar");
}
Another approach is to use a $sidebar service - this is somewhat similar to how $modal works in angularui-bootstrap, but could be more simplified.
Well, if you have a directive on a button and the element you need is outside the directive, you could pass the class of the element you need to toggle as an attribute
<button my-directive data-toggle-class="sidebar">open</button>
Then in your directive
App.directive('myDirective', function() {
return {
restrict: 'A',
link: function(scope, element, attrs) {
angular.element('.' + attrs.toggleClass).toggleClass('active');
}
};
}
You won't always have the link element argument match up with what you need to manipulate unfortunately. There are many "angular ways" to solve this though.
You could even do something like:
<div ng-init="isOpen = false" class="sidebar" ng-class="{'active': isOpen}" ng-click="isOpen = !isOpen">
...
</div>
The best way for directive to communicate with each other is through events. It also keeps with the separation of concerns. Your button could $broadcast on the $rootScope so that all scopes hear it. You would emit and event such as sidebar.open. Then the sidebar directive would listen for that event and act upon it.

Polymer custom element attribute access or send

I'm searching for a way to access an attribute on a Polymer custom element from the DOM
or to send data from Polymer.register to the DOM.
This really simple element below takes two values and multiplies them, placing the result in its result attribute.
How can I access this result from the outside?
<element attributes='value times result' name='value-box'>
<template>
<p>{{result}}</p>
</template>
<script>
Polymer.register(this, {
ready: function() {
if (this.value != null && this.times != null) {
this.result = this.value * this.times;
}
}
});
</script>
</element>
result is a property on your element just like times and value. You can access it from outside JS, as you would any property on a normal HTML element. For example:
<value-box value="2" times="10"></value-box>
<script>
document.querySelector('value-box').result;
</script>
Internal to your element, what you want is to keep the result computed property up to date as times/value change. There are a couple of ways to do that. One is to use <property>Changed watchers [1]:
<element name="value-box" attributes="value times result">
<template>
<p>result: {{result}}</p>
</template>
<script>
Polymer.register(this, {
valueChanged: function() {
this.result = this.value * this.times;
},
timesChanged: function() {
this.result = this.value * this.times;
}
});
</script>
</element>
Demo: http://jsbin.com/idecun/2/edit
Alternatively, you can use a getter for result:
Polymer.register(this, {
get result() {
return this.value * this.times;
}
});
Demo: http://jsbin.com/oquvap/2/edit
Note For this second case, if the browser doesn't support Object.observe, Polymer will setup a timer to dirty check result. This is why you see "here" printed in the console for this second example. Run the same thing in Chrome Canary with "Experimental WebKit features" enabled in about:flags, and you won't see the timer. Yet another reason why I can't wait for Object.observe to be everywhere! :)
Hope this helps.
Just wanted to add a useful follow up to this (Even though the question has been answered).
My follow up is in response to the following comment on the actual answer:
I'm curious as to why selection with jQuery didn't work. Does it not recognize Custom Elements? – CletusW Jul 8 '13 at 19:57
The most likely reason jQuery didn't see your element is because it was not fully formed by the browsers run time at that point.
I ran into this problem while developing my ASP.NET MVC + polymer js sample app on my github page, and essentially what I was trying to do was call methods and access properties on my polymer object before polymer had made everything usable.
Once I moved the code I was using into a button click (So I could trigger it manually after I visually could see my component was ready) everything worked fine.
For now, if you try to access anything too soon, EG: in your jQ doc.ready handler, there's a good chance you'll run into all sorts of daft problems like this.
If you can find a way of delaying your action, or even better using polymer signals to signal from the components ready handler to an outside agent, that sets a flag telling you the component is ready, then you can sort this easily.