Dynamically determine existence of Ember component? - javascript

I was looking through the ember-htmlbars package and discovered this util for determining if a component is available? https://github.com/emberjs/ember.js/blob/master/packages/ember-htmlbars/lib/utils/is-component.js
Is there any way to use this from my app? I'm building a dashboard-type interface and some of the dashboard widgets have optional actions. In essence I'd like to do something like:
<div class="panel panel-default">
<div class="panel-body">
{{component model.displayComponent model=model}}
</div>
{{#if isComponent(model.actionComponent) }} <!-- this would be a property -->
<div class="panel-footer">
{{component model.actionComponent model=model}}
</div>
{{/if}}
</div>
My fallback is to put a blank action component for each of my widgets that don't have one, but it would be cleaner to be able to check to see if they exist first.

you can create helper is-component
export default Ember.Helper.extend({
compute([name]) {
return this.container.registry.has('component:' + name);
}
})
and use it like {{#if (is-component model.actionComponent) }}

Related

Is there a way to tell if a yielded component's has-block is empty?

I am trying to make a contextual component that can provide a default header when one is not passed in the yielded block. Normally, I could use the has-block helper to tell if a component has block content but this component will always have block content. I think what I need is a way to tell if the yielded component has block content. For example:
{{! panel.hbs }}
<header>
{{yield header=(component "blank-template")}}
{{#if (yieldedHeaderEmpty)}}
<h1>My Default Header</h1>
{{/if}}
</header>
<section>
{{yield body=(component "blank-template")}}
</section>
<footer>
{{yield footer=(component "blank-template")}}
</footer>
I only want the default header to show when the yielded "header" section is empty. I could create a header component which then could use the has-block like so:
{{! panel.hbs }}
<header>
{{yield header=(component "my-header")}}
</header>
...
{{! MyHeader.hbs}}
{{#if (has-block)}}
{{yield}}
{{else}}
<h1>My Default</h1>
{{/if}}
Functionally I think this will work but the only purpose of the "my-header" component would be for this "panel" component and seems wasteful. Open to ideas. Thanks!
Ember core recently accepted an RFC for "named blocks" which is designed exactly for this purpose. https://github.com/emberjs/rfcs/pull/460. With this feature, we can name the yieldable blocks and access them with their names, like:
template.hbs:
<Panel>
<:header>This is a customized header</:header>
</Panel>
panel.hbs:
{{#if (has-block "header")}}
{{yield to="header"}}
{{else}}
<header> This is the default header </header>
{{/if}}
This is yet to be landed in the Ember core. Luckily we have an official polyfill that supports this feature from 3.12.4: https://github.com/ember-polyfills/ember-named-blocks-polyfill

A component is not receiving any of it's #Input values

I use Angular 8 and I have 3 components:
CalculationFirst : app-calculation-first
CalculationSecond : app-calculation-second
CalculationThird : app-calculation-third
CalculationFirst is the "parent" component and has CalculationSecond as a "child" in a way that I use CalculationSecond inside html template of CalculationFirst.
So in CalculationFirst.component.html it looks like this:
<div class="container">
<form #f="ngForm" (ngSubmit)="submit()" novalidate *ngIf="settings.isloaded == true">
<div class="row">
<app-calculation-second [elements]="settings.seconds.elements" [epics]="epics"[types]="types" (opendialoglistevent)="openDialogList($event)"></app-calculation-second>
</div>
</form>
</div>
Then CalculationSecond.component.html looks like this:
<div class="col-lg-2 box">
<div class="row">
<div class="row content" *ngFor="let element of elements;">
<div class="col-lg-14">
<app-calculation-third [elem]="element" [types]="types"></app-calculation-third>
</div>
</div>
</div>
</div>
And finally CalculationThird.component.html looks like :
<div class="col-lg-14">
TEST_TEXT_1
{{elem}}
TEST_TEXT_2
</div>
Now, the problem is if I were to just look at my rendered page, only TEST_TEXT_1 page shows, {{elem}} does not render and neither does TEST_TEXT_2. If I am to check the value of elem, it is undefined. I would expect to get an error that I am trying to display undefined, however I do not get anything. My environment.ts is not set to production, so I do not know if my error is not big enough for angular to inform me. (On a tangent, I don't even get an error if I use a json object in an *ngFor instead of an enumerable (Array), but I'm still not sure if I have some lacking setting).
I have also tried, when I use the third component, to not pass the element from *ngFor but to pass a handmade object {'elemId':0}, this had the same result.
If I move :
<app-calculation-third [elem]="element" [types]="types"></app-calculation-third>
to CalculationFirst, making it a direct child, it works perfectly fine, regardless of what I put in there. Is there a limit to the amount of child components you can have? Am I missing something?
*Edit: Also for those who suggested it is the *ngFor or the usage of different variables, I can make CalculationSecond look like the following and still it doesn't work:
<div class="col-lg-2 box">
<div class="row">
<div class="row content" *ngFor="let element of elements;">
<div class="col-lg-14">
</div>
</div>
</div>
<app-calculation-third [elem]="{ elementId: '' }" [types]="types"></app-calculation-third>
</div>
You are passing the elements to seconds input.
[seconds]="settings.seconds.elements"
But you use elements in your app-second-calculation element.

Vue js error: Component template should contain exactly one root element

I don't know what the error is, so far I am testing through console log to check for changes after selecting a file (for uploading).
When I run $ npm run watch, i get the following error:
"Webpack is watching the files…
95% emitting
ERROR Failed to compile with 1 errors
19:42:29
error in ./resources/assets/js/components/File.vue
(Emitted value instead of an instance of Error) Vue template syntax
error:
Component template should contain exactly one root element. If you
are using v-if on multiple elements, use v-else-if to chain them
instead.
# ./resources/assets/js/components/AvatarUpload.vue 5:2-181 #
./resources/assets/js/app.js # multi ./resources/assets/js/app.js
./resources/assets/sass/app.scss"
My File.vue is
<template>
<div class="form-group">
<label for="avatar" class="control-label">Avatar</label>
<input type="file" v-on:change="fileChange" id="avatar">
<div class="help-block">
Help block here updated 4 🍸 ...
</div>
</div>
<div class="col-md-6">
<input type="hidden" name="avatar_id">
<img class="avatar" title="Current avatar">
</div>
</template>
<script>
export default{
methods: {
fileChange(){
console.log('Test of file input change')
}
}
}
</script>
Any ideas on how to solve this? What is actually the error?
Note This answer only applies to version 2.x of Vue. Version 3 has lifted this restriction.
You have two root elements in your template.
<div class="form-group">
...
</div>
<div class="col-md-6">
...
</div>
And you need one.
<div>
<div class="form-group">
...
</div>
<div class="col-md-6">
...
</div>
</div>
Essentially in Vue you must have only one root element in your templates.
For a more complete answer: http://www.compulsivecoders.com/tech/vuejs-component-template-should-contain-exactly-one-root-element/
But basically:
Currently, a VueJS template can contain only one root element (because of rendering issue)
In cases you really need to have two root elements because HTML structure does not allow you to create a wrapping parent element, you can use vue-fragment.
To install it:
npm install vue-fragment
To use it:
import Fragment from 'vue-fragment';
Vue.use(Fragment.Plugin);
// or
import { Plugin } from 'vue-fragment';
Vue.use(Plugin);
Then, in your component:
<template>
<fragment>
<tr class="hola">
...
</tr>
<tr class="hello">
...
</tr>
</fragment>
</template>
You need to wrap all the html into one single element.
<template>
<div>
<div class="form-group">
<label for="avatar" class="control-label">Avatar</label>
<input type="file" v-on:change="fileChange" id="avatar">
<div class="help-block">
Help block here updated 4 🍸 ...
</div>
</div>
<div class="col-md-6">
<input type="hidden" name="avatar_id">
<img class="avatar" title="Current avatar">
</div>
</div>
</template>
<script>
export default{
methods: {
fileChange(){
console.log('Test of file input change')
}
}
}
</script>
if, for any reasons, you don't want to add a wrapper (in my first case it was for <tr/> components), you can use a functionnal component.
Instead of having a single components/MyCompo.vue you will have few files in a components/MyCompo folder :
components/MyCompo/index.js
components/MyCompo/File.vue
components/MyCompo/Avatar.vue
With this structure, the way you call your component won't change.
components/MyCompo/index.js file content :
import File from './File';
import Avatar from './Avatar';
const commonSort=(a,b)=>b-a;
export default {
functional: true,
name: 'MyCompo',
props: [ 'someProp', 'plopProp' ],
render(createElement, context) {
return [
createElement( File, { props: Object.assign({light: true, sort: commonSort},context.props) } ),
createElement( Avatar, { props: Object.assign({light: false, sort: commonSort},context.props) } )
];
}
};
And if you have some function or data used in both templates, passed them as properties and that's it !
I let you imagine building list of components and so much features with this pattern.
Component template should contain exactly one root element. If you are using v-if on multiple elements, use v-else-if to chain them instead.
The right approach is
<template>
<div> <!-- The root -->
<p></p>
<p></p>
</div>
</template>
The wrong approach
<template> <!-- No root Element -->
<p></p>
<p></p>
</template>
Multi Root Components
The way around to that problem is using functional components, they are components where you have to pass no reactive data means component will not be watching for any data changes as well as not updating it self when something in parent component changes.
As this is a work around it comes with a price, functional components don't have any life cycle hooks passed to it, they are instance less as well you cannot refer to this anymore and everything is passed with context.
Here is how you can create a simple functional component.
Vue.component('my-component', {
// you must set functional as true
functional: true,
// Props are optional
props: {
// ...
},
// To compensate for the lack of an instance,
// we are now provided a 2nd context argument.
render: function (createElement, context) {
// ...
}
})
Now that we have covered functional components in some detail lets cover how to create multi root components, for that I am gonna present you with a generic example.
<template>
<ul>
<NavBarRoutes :routes="persistentNavRoutes"/>
<NavBarRoutes v-if="loggedIn" :routes="loggedInNavRoutes" />
<NavBarRoutes v-else :routes="loggedOutNavRoutes" />
</ul>
</template>
Now if we take a look at NavBarRoutes template
<template>
<li
v-for="route in routes"
:key="route.name"
>
<router-link :to="route">
{{ route.title }}
</router-link>
</li>
</template>
We cant do some thing like this we will be violating single root component restriction
Solution
Make this component functional and use render
{
functional: true,
render(h, { props }) {
return props.routes.map(route =>
<li key={route.name}>
<router-link to={route}>
{route.title}
</router-link>
</li>
)
}
Here you have it you have created a multi root component, Happy coding
Reference for more details visit: https://blog.carbonteq.com/vuejs-create-multi-root-components/
In addition to Bert and blobmaster responses:
If you need to remove the root element from the DOM you can exploit css and use display: value on the root element.
Bit of a misleading error.
What fixed it on my side was the fact that I had an additional </div> without an opening <div>.
I spotted it using Find/Replace on "div" which gave an odd number.
Wrap everything in one div and it will resolve the issue.
For example,
div
----div
----/div>
----div>
----/div>
/div
It is similar concept to React.js
For vue 3 they removed this constraint in template syntax :
<template>
<header>...</header>
<main v-bind="$attrs">...</main>
<footer>...</footer>
</template>
but it's still existing in JSX syntax :
Incorrect ❌
setup(props,{attrs}) {
return ()=>(
<header>...</header>
<main {..attrs}>...</main>
<footer>...</footer>
)
}
Correct ✔
setup(props,{attrs}) {
return ()=>(
<>
<header>...</header>
<main {..attrs}>...</main>
<footer>...</footer>
</>
)
}
I experienced this kind of issue and the issue was fixed by adding a main parent div tag or section if it is a section type of component.
<div class="list-of-friends">
<h3>Hello World</h3>
</div>
I was confused as I knew VueJS should only contain 1 root element and yet I was still getting this same "template syntax error Component template should contain exactly one root element..." error on an extremely simple component. Turns out I had just mispelled </template> as </tempate> and that was giving me this same error in a few files I copied and pasted. In summary, check your syntax for any mispellings in your component.
instead of using this
Vue.component('tabs', {
template: `
<div class="tabs">
<ul>
<li class="is-active"><a>Pictures</a></li>
<li><a>Music</a></li>
<li><a>Videos</a></li>
<li><a>Documents</a></li>
</ul>
</div>
<div class="tabs-content">
<slot></slot>
</div>
`,
});
you should use
Vue.component('tabs', {
template: `
<div>
<div class="tabs">
<ul>
<li class="is-active"><a>Pictures</a></li>
<li><a>Music</a></li>
<li><a>Videos</a></li>
<li><a>Documents</a></li>
</ul>
</div>
<div class="tabs-content">
<slot></slot>
</div>
</div>
`,
});
Just make sure that you have one root div and put everything inside this root
<div class="root">
<!--and put all child here --!>
<div class='child1'></div>
<div class='child2'></div>
</div>
and so on

bind values to application.hbs from different routes

In Ember.js (version 2.5.1), I have the following code in my application.hbs:
<div class="content-wrapper">
<section class="content-header">
<h1>
{{title}}<small>{{description}}</small>
</h1>
</section>
<section class="content">
{{outlet}}
</section>
</div>
Where should I declare {{title}} and {{description}} properties so they can change depending on the current route? I thought of generating an application controller (ember g controller application) which works fine for the index route, however, I am unable to change these properties if I navigate to other routes in my application.
I would design my application.hbs like this
{{partial "app_navbar"}}
<div class="container">
{{outlet}}
</div>
{{partial "app_footer"}}
And to create a route dependent header i would make a component content-header
templates/components/content-header.hbs
<div class="content-header">
<h1> {{params.title}} - <small> {{params.description}} </small> </h1>
</div>
And in your custom route's controllers
export default Ember.Controller.extend({
navbarParams: {
title: 'Foo',
description: 'Bar'
}
});
And in your custom route's template
{{content-header params=navbarParams}}
{{! Your other content here}}

Creating complex static Markup with ember.js

I am trying to render my application template, which is getting very complicated, so I want to split it up into separate <script type="text/x-handlebars">'s
I think I want to use the {{view}} helper, but I don't know. Let's say I have this:
<script type="text/x-handlebars" data-template-name="application">
<div id="wrapper" class="clearfix">
<hgroup>
<header>
<div class="logo"></div>
<h1 id="facilityName">{{facilityName}}</h1>
<div id="sessionInfo">
<a id="userName">{{user}}</a>
<a id="logOut" href="../logout">log out</a>
</div>
</header>
</hgroup>
{{view App.breadcrumbsView}}
</div>
</script>
And I want to load this next template inside of the one above:
<script type="text/x-handlebars" id="breadcrumbs">
<div id="breadcrumbs">
<p>
Network
{{#each breadcrumbObj}}
<span>></span><a {{bindAttr href="link"}}>{{name}}</a>
{{/each}}
</p>
</div>
</script>
Right now I am trying to do this with this code from my app.js
App.breadcrumbsView = Ember.View.create({
templateName: 'breadcrumbs',
breadcrumbObj: Util.breadcrumbs()
});
This is not working and I am getting Uncaught Error: assertion failed: Unable to find view at path 'App.breadcrumbsView'
Thanks
I think that you declared your App using var keyword. So it's not visible in handlebars template.
Change this
var App = Ember.Application.create();
To this
App = Ember.Application.create();
And you have to use extend instead of create when creating your views. Because ember will handle the object lifecycle like: create, destroy, add and remove binding etc.
#Marcio is right, you should .extend() and let the framework create it for you automatically by request. Furthermore when you extend the best practice is to use capitalized names like App.BreadcrumbsView.
See here a demo how it renders correctly doing it this way.
Hope it helps.

Categories

Resources