I've just jumped in to OpenUI5, and had a problem where I couldn't get navigation targets to open up in aggregations within sub views. I found an answer on Stack Overflow that said that I needed to specifically name each View in the parent hierarchy to stabilise the name - this mostly worked, and I've been able to get my routing and targets to function.
My problem now is that the ID prefix of my item is __component0. I cannot find any way to change the name of this part, so really I'm not fully in control of my IDs.
I've tried changing the sId of the component before and after initialisation, but then the app doesn't function. I've also set both sap.app.componentName and sap.ui5.componentId, and neither are used. It appears that component0 is constructed by getting the type name of the controller class, but that has to be called Component.js.
Does anybody know how to change this value, or otherwise derive a control ID in the DOM that is fully defined in code?
In response to boghyon, I don't doubt that setting the ID via the factory function works, I don't know how to apply that ID value or factory function to the style of code that I have. This style comes from the SAP starter template on GitHub. Simply specifying id: "mycomponentname" in the extend object does not work.
So how would I go about refactoring what I have to the factory function format?
sap.ui.define([
"sap/ui/core/UIComponent",
"sap/ui/Device",
"com/uk/ia/air/cpanel/model/models"
], function(UIComponent, Device, models) {
"use strict";
return UIComponent.extend("com.uk.ia.air.cpanel.Component", {
metadata: {
manifest: "json"
},
/**
* The component is initialized by UI5 automatically during the startup of the app and calls the init method once.
* #public
* #override
*/
init: function() {
// call the base component's init function
UIComponent.prototype.init.apply(this, arguments);
// set the device model
this.setModel(models.createDeviceModel(), "device");
// create the views based on the url/hash
this.getRouter().initialize();
}
});
});
The ID of a component can be manually assigned when instantiating the component
Either with sap/ui/core/Component#createAPI
Or with new ComponentContainer(/*...*/)API
With ComponentContainer
E.g. in index.html when bootstrapping:
<head>
<!-- ... -->
<script id="sap-ui-bootstrap" src="https://ui5.sap.com/resources/sap-ui-core.js"
data-sap-ui-oninit="module:sap/ui/core/ComponentSupport"
data-sap-ui-async="true"
data-sap-ui-resourceroots='{"demo": './'}'
...
></script>
</head>
<body id="content" class="sapUiBody">
<div data-sap-ui-component
data-id="myComponentContainer"
data-name="demo"
data-height="100%"
data-settings='{"id": "myComponent"}'
></div>
</body>
The module, that can create a ComponentContainer in index.html, is sap/ui/core/ComponentSupport. Once it's loaded, it will look for the HTML element that has the attribute data-sap-ui-component. That HTML element should provide constructor settings for the ComponentContainer which provides settings for the component constructor same as in Component.create.
With Component.create()
Component.create({ // Component required from "sap/ui/core/Component"
id: "myComponent", // or this.getView().createId("myComponent") if applicable
name: "demo",
// ...
});
For more information, see Stable IDs: All You Need to Know.
Additionally, every view must have an ID as well so that the given component ID is included in the full ID of an element. Otherwise, the component ID won't be available even if we defined it in the component factory function. View IDs can be set via the property id and viewId respectively in the application descriptor (manifest.json):
sap.ui5 / rootView /id
sap.ui5 / routing / targets /targetName/viewId
You can change the autogenerated ID of the extended component if you override the constructor of the UIComponent. With your code it should look like this:
return UIComponent.extend("com.uk.ia.air.cpanel.Component", {
constructor: function(sId, mSettings) {
UIComponent.call(this, "MyComponentId", mSettings);
},
metadata: {
manifest: "json"
},
/**
* The component is initialized by UI5 automatically during the startup of the app and calls the init method once.
* #public
* #override
*/
init: function() {
// call the base component's init function
UIComponent.prototype.init.apply(this, arguments);
// set the device model
this.setModel(models.createDeviceModel(), "device");
// create the views based on the url/hash
this.getRouter().initialize();
}
});
});
Related
I have two components, one is parent component and another one is child component which is a modal popup.
Parent component
<parent-component>
<!-- this is a modal popup -->
<child-component v-bind:message="childMessage"></child-component>
Open model
</parent-component>
<script>
export default {
data: function() {
return {
childMessage:{}
}
},
methods:{
openModal: function(id){
axios.get('api/message/'+id)
.then(response => {
this.childMessage = response.data;
})
.catch(error => {
console.log(error);
})
this.showModal = true
}
}
}
</script>
Child component
<!-- this is a popup modal -->
<child-component>
<h1>{{ message.title }}</h1>
</child-component>
<script>
export default {
props:{
message: {},
}
</script>
In parent component, I trigger the modal and request ajax at the same time.
And I can pass the ajax data to child component correctly.
But if I open the console, there is an error
Cannot read property 'title' of undefined
(Although I can see the data is working fine and it's already in html page)
It seems the appending data {{ childMessage.title }} run first (before the ajax request).
1 - How can I append the data correctly, probably after the ajax request.
2 - Do I need to check the condition for undefined value?
I don't see where you use showModal but I suppose it's a sort of switch to display or not the child-component. If it's the case the error can come from the fact that you set showModal to true just after the call to the API. But this call is asynchronous you should probably move this.showModal = true in the success callback under this.childMessage = response.data;.
If you do that the message prop will be initialize at the moment the child component is rendered.
Also pay attention to your prop type, as #ittus mention message seems to be a String according to the default value in the child-component but you use it like an object in the template.
In this case, you have to check the condition for undefined value because child component is being rendered before the message API call ends. You can do it in this way,
<child-component>
<h1 v-if = "message">{{ message.title }}</h1>
</child-component>
I have a laravel app and a Vue instance attached to the body (or a div, just inside the body).
const app = new Vue({
el: '#app'
});
I think it makes sense to use the Vue instance for stuff relating to the layout (eg header, nav, footer logic).
Now I have a form that is visible on a specific route (e.g. example.com/thing/create). I want to add some logic to it, e.g. hiding a field based on selected option in the form. It is logic meant for just this form (not to be reused). I prefer not to put all the logic inline with the form but put it in the app.js. I could put it in the Vue instance bound to the body but that sounds odd as it only applies to the form that is much deeper into the dom.
I want to leave the markup of the form in the blade template (that inherits the layout).
I tried creating a component but am not sure how to bind this inside the main Vue instance. What is the best way to handle things for this form, put it in the app.js and have it somewhat structured, putting the variables somewhat into scope. Or is it really necessary to remove the main Vue instance bound to the full layout code?
What I tried was something like this, but it does not work (attaching it to the <form id="object-form"> seems to fail:
var ObjectForm = {
template: function() { return '#object-form'},
data: function() {
return {
selectedOption: 1
}
},
computed: {
displayField: function() {
// return true or false depending on form state
return true;
}
}
};
Things do work if I remove the #app Vue instance or when I put everything directly in the app Vue instance. But that seems messy, if I have similar variables for another form they should be seperated somewhat.
I would appreciate some advice regarding the structure (differentiate page layout and page specific forms) and if possible some example to put the form logic inside the main app.js.
I hope this helps kind of break things down for you and helps you understand Vue templating.
It is best to take advantage of Vue's components. For you it would look something like this. Some of this code depends on your file structure, but you should be able to understand it.
In your app.js file (or your main js file)
Vue.component('myform',require('./components/MyForm.vue'));
const app = new Vue({
el: "#app"
});
Then create the MyForm.vue file
<template>
<form>
Put Your Form Markup Here
</form>
</template>
<script>
// Here is where you would handle the form scripts.
// Use methods, props, data to help set up your component
module.exports = {
data: function() {
return {
selectedOption: 1
}
},
computed: {
displayField: function() {
// return true or false depending on form state
return true;
}
},
methods: {
// add methods for dynamically changing form values
}
}
</script>
Then you will be able to just call in your blade file.
<myform></myform>
I found out how to do it. The trick was to use an inline template. Surround the form in the view with:
<object-form inline-template>
<form>...</form>
</object-form>
Where object-form is the name of the component. In the ObjectForm code above I remove the template, like this:
var ObjectForm = {
data: function() {
return {
selectedOption: 1
}
},
computed: {
displayField: function() {
// return true or false depending on form state
return true;
}
}
};
I attach the component within the root vue app like this:
const app = new Vue({
el: 'body',
components: {
'object-form': ObjectForm
}
});
This way I can use the form as it was generated from the controller and view and I can separate it from the root (attached to body) methods and properties.
To organize it even better I can probably store the ObjectForm in a seperate .vue file the way #Tayler Foster suggested.
I am working on a SAPUI5 application. I have an XML view which contains an XML Fragment and a Button to save.
The fragment contains a few controls like drop-down, text field and a table.
When I press on the save button, I need to get all the rows in the table and call an OData update service.
The problem is in the onSave method in view controller. I get an error while accessing the table using its ID. Can anyone help me and advice how can I access controls used in fragments by their ID in the controller?
Here is the code snippet:
View:
<mvc:View xmlns:mvc="sap.ui.core.mvc" xmlns:core="sap.ui.core" xmlns:form="sap.ui.layout.form" xmlns="sap.m">
<Page>
...
<form:SimpleForm>
<core:Fragment id ="fr1" fragmentName="first" type="XML" />
<Button id="id1" press="onSave" />
</form:SimpleForm>
</Page>
</mvc:View>
Fragment definition:
<core:FragmentDefinition xmlns="sap.m" xmlns:core="sap.ui.core">
<Table id="tab1" mode="MultiSelect">
...
</Table>
</core:FragmentDefinition>
Controller:
sap.ui.controller("view", {
onSave: function() {
//var tab = this.getView().byId("tab1"); // Not working
var tab = sap.ui.getCore().byId("tab1"); // Not working
},
// ...
});
Accessing controls inside a fragment depends on how your fragment was created in the first place. Here is a list of cases with respective API to use to get the control reference.
Given:
this as a reference to the current controller instance
Fragment required from the module sap/ui/core/Fragment
<MyControl id="controlId"/> in the fragment definition
API to choose
👉this.byId("controlId");
... if the fragment was created with the view ID (either indirectly or directly):
this.loadFragment({ name: "..." }); // id: view ID given by default, API since 1.93
<!-- In the view embedding the fragment declaratively: -->
<core:Fragment fragmentName="..." type="XML"/><!-- id = view ID given by default -->
Fragment.load({ // API since 1.58
id: this.getView().getId(),
name: "...",
controller: this,
});
sap.ui.xmlfragment(this.getView().getId(), "...", this); // Deprecated
Resulting global ID: "componentId---viewId--controlId" *
👉this.byId(Fragment.createId("fragmentId", "controlId"));
... if a fragment ID was given with the view ID combined:
this.loadFragment({ id: this.createId("fragmentId"), name: "..." });
<core:Fragment id="fragmentId" fragmentName="..." type="XML"/>
Fragment.load({
id: this.createId("fragmentId"),
name: "...",
controller: this,
});
sap.ui.xmlfragment(this.createId("fragmentId"), "...", this); // Deprecated
Resulting global ID: "componentId---viewId--fragmentId--controlId" *
👉Fragment.byId("fragmentId", "controlId");
... if only the fragment ID was given without combining with the view ID:
this.loadFragment({
id: "fragmentId",
name: "...",
autoPrefixId: false, // Explicitly disabled view ID as prefix
});
Fragment.load({
id: "fragmentId",
name: "...",
controller: this,
});
sap.ui.xmlfragment("fragmentId", "...", this); // Deprecated
Resulting global ID: "fragmentId--controlId" *
👉sap.ui.getCore().byId("controlId");
... if no ID to prefix was given. The below settings are not recommended as all control IDs within the fragment definition will be registered globally without any prefix. The uniqueness of the IDs is not guaranteed!
this.loadFragment({ name: "...", autoPrefixId: false }); // Not recommended if no id
Fragment.load({ name: "...", controller: this }); // Not recommended
sap.ui.xmlfragment("demo.view.MyFragment", this); // Deprecated
Resulting global ID: "controlId"
* Do not rely on the resulting global ID, for example, concatenating ID parts manually in your application. Always use the dedicated APIs mentioned above such as byId and createId. See Stable IDs: All You Need to Know.
Favor model-first approach over byId
Instead of accessing the fragment controls directly, consider manipulating the UI via data binding. Changes in the model will be reflected in the UI automatically, and, if two-way binding is enabled, user inputs from the UI will be stored in the model directly.
SAP Fiori elements guidelines
When developing Fiori elements extensions, make sure to adhere to the documented compatibility guidelines, especially regarding byId:
[...] Don't access or manipulate SAP Fiori elements' internal coding.
[...] Must not access any UI elements that are not defined within your view extensions.
âš Caution
If you do not adhere to this guideline, your app may not work with future SAPUI5 versions because SAP Fiori elements might exchange controls for new ones that have a different API.
Looking at the OpenUI5 code at GitHub, it seems that the Fragment delegates the local ID generation to the containing view if the <Fragment/> itself does not have an explicit ID.
So your code this.getView().byId("tab1") should work as soon as you remove the id="fr1" attribute from your <Fragment/> element.
When using explicit IDs there is a static Fragment.byId method to retrieve the control. I guess you have to use it like this:
// Fragment required from "sap/ui/core/Fragment"
var fragmentId = this.getView().createId("fr1");
var tab = Fragment.byId(fragmentId, "tab1");
To make it work without explicit fragment ID and without static Fragment.byId() I used the following code snippet:
var prefix = this.getView().createId("").replace("--", "");
var fragment = sap.ui.xmlfragment(prefix, "-- XML fragment name --", this);
after this you can use this.getView().byId("tab1") as with any other control.
I want to access a view's controller from a custom module with some utility functions. Basically you can do this that way:
var oController = sap.ui.getCore().byId("__xmlview1").getController();
The problem is that the above coding will not work in a real environment because __xmlview1is dynamically created by the framework. So I tried to find a possibility to set the ID of the view during instantiation. The problem is - I could not find one:
Trying to give the view an ID in the view.xml file does not work:
<mvc:View
controllerName="dividendgrowthtools.view.dividendcompare"
id="testID"
xmlns="sap.m"
...
Trying to set the ID in the router configuration of the component does not work either:
...
name: "Dividend Compare",
viewId: "test",
pattern: "Dividend-Compare",
target: "dividendcompare"
...
The problem is that I do not have direct control over the instantiation of the XML view - the component respectively the router does it.
So, is there a solution for that problem? Or at least a save way to get the view ID by providing the name of the view?
You should have a look at the SAPUI5 EventBus.
I am pretty sure, you want to let the controller to do something with the dividentcompare view. With the SAPUI5 Eventbus, you can publish actions from one controller to another witout braking MVC patterns.
In your dividendcompare.controller.js:
onInit : function() {
var oEventBus = sap.ui.getCore().getEventBus();
oEventBus.subscribe("MyChannel", "doStuff", this.handleDoStuff, this);
[...]
},
handleDoStuff : function (oEvent) {
var oView = this.getView();
[...]
}
Now, in your anothercontroller.controller.js:
onTriggerDividendStuff : function (oEvent){
var oEventBus = sap.ui.getCore().getEventBus();
oEventBus.publish("MyChannel", "doStuff", { [optional Params] });
}
You are now able to get the view from the dividentcontroller in every case from every other controller of your app. You dont access the view directly, this would brake MVC patterns, but can pass options to its controller and do the handling there.
I render a jsrender template in template object.
var colorTmpl = {
"color": jQuery.templates("<span>{{:~val}}</span>"),
}
var jsrendertmpl = {};
jQuery.extend(jsrendertmpl, colorTmpl);
If I access the jsrendertmpl.colorTmpl.render() it will render the color template. If I include this tmpl template tag within other template like this
{colorArr: ["white", "blue", "black"]}
{{for colorArr tmpl="jsrendertmpl.colorTmpl"/}}
It will not include that subtemplate into parent template. So I add a custom tag to include this subtemplate within object
includetmpl: function(tmplname){
return Template.render(tmplVal, jObj);
},
If I include the subtemplate in parent template {{:~val}} cannot be recognised
Parent Template with customised include tag
{{for colorArr itemVar='~val'}}
{{includetmpl "color"}}
{{/for}}
Parent template without subtemplate (It works fine)
{{for colorArr itemVar='~val'}}
<span>{{:~val}}</span>
{{/for}}
Is there any way to access that value. I used a {{setvar}} and {{:~getvar}} for workaround but need a permanent solution.
Thanks in advance.
There are a few ways you can implement that scenario - i.e. to provide different templates based on a data parameter that you pass in.
For example you can pass in the template as a helper:
$.views.helpers("myTemplates", {
color: $.templates("<span>{{:}}</span>"),
colorVal: $.templates("<span>{{:~val}}</span>")
});
And use it as in
{{for colorArr tmpl=~myTemplates.color/}}
or if tmplVar is your chosen template: "color"
{{for colorArr tmpl=~myTemplates[tmplVar]/}}
You can also create a custom tag, as you suggest, in either of the following ways:
$.views.tags("includetmpl", {
init: function(tagCtx) {
this.template = myTemplates[tagCtx.props.template];
}
});
or
$.views.tags("includetmpl2", {
render: function(val) {
return myTemplates[this.tagCtx.props.template].render(val, this.ctx);
}
});
(Note how I pass the context also to my render function, in the second version, so that ~val will be available...)
Then use it as in the following:
{{for colorArr}}{{includetmpl template="color"/}}{{/for}}
or
{{for colorArr itemVar="~val"}}{{includetmpl template="colorVal"/}}{{/for}}
See https://jsfiddle.net/BorisMoore/coezx8xj/ for all of the above...
I will change the title of your question to make it clearer.