Custom Controls Framework - Styles
– 7 MinutesIf you followed the previous posts, you should have a very basic control that let's you input a number and show it in a specified color if the value is below a threshold. Although the control is functional, it doesn't blend in with the out-of-the-box controls since it doesn't use the same styles. There are three different methods we can use to implement styles on custom controls, and we will explore those in this post.
Style Sheets
The first way involves using Cascading Style Sheets (CSS). We do this by creating a new .css file, including it in our control's directory, and referencing it in the control manifest.
First, we'll create the CSS file, copying some of the styles from the out-of-the-box controls.
.percentage-control input {
flex: 1;
color: rgb(0, 0, 0);
border: 1px solid transparent;
height: 2.5rem;
width: 100%;
font-size: 1rem;
font-weight: 600;
text-overflow: ellipsis;
line-height: 2.5rem;
box-sizing: border-box;
padding: 0px 0.5em;
outline: none !important;
}
.percentage-control input:focus, .percentage-control input:hover {
font-weight: 400;
border: 1px solid rgb(102, 102, 102);
}
...and then we'll update the control manifest to add a reference to it. By doing this, Dynamics 365 will take care of automatically loading it for us when it loads the control.
<manifest>
<control...>
...
<resources>
<code path="PercentageControl.js" order="1" />
<css path="css/PercentageControl.css" order="2" />
<resx path="strings/PercentageControl.1033.resx" version="1.0.0" />
</resources>
</control>
</manifest>
If you recall from the previous posts, we use the createElement function from the factory object to generate the React element for our control. Normally we would be able to add a className to our props, but since the createElement function is a wrapper, we are limited by how it works -- and it doesn't let us set a className on an input element. However, we can assign a class to a container element.
We'll update the updateView function as follows:
updateView(context) {
this.props.value = context.parameters.value.raw;
this.props.style = this.getStyle(context);
let textInput = context.factory.createElement("TEXTINPUT", this.props);
let containerProps = { className: "percentage-control" };
let container = context.factory.createElement("CONTAINER", containerProps, [ textInput ]);
return container;
}
Notice that the second createElement call takes a third array parameter. This is how we can build more complex controls with multiple elements.
You can download the solution which uses the style sheet method here: CustomControls_1_0_0_2.zip.
Helper Class
The second method we can use to generate styles is the ThemingHelper class. This class exists in the Microsoft Virtual Control Llibrary, so in order to use it we must add a reference to to our control manifest. As you can guess, this library also contains other useful classes for virtual controls, and we will explore that more in the next section.
To add the refernce, update the control manifest as follows:
<manifest>
<control...>
...
<resources>
<library name="msvcontrol" version=">=1" order="1">
<packaged_library path="libs/msvcontrollib-1.0.0.js" version="1.0.0" />
</library>
<code path="PercentageControl.js" order="2" />
<resx path="strings/PercentageControl.1033.resx" version="1.0.0" />
</resources>
</control>
</manifest>
We'll also drop a copy of msvcontrollib-1.0.0.js into our control folder (download the solution at the end of this section).
The ThemingHelper class has a few methods on it. In a nutshell, they generate a style object that contains the same styles used by other controls. Here is the type definition for the class.
namespace MscrmControls.FieldControls {
class ThemingHelper {
isHeaderCell(scope);
isFooterCell(scope);
getCommonStyle(theming, deviceSizeMode, scope, isRTL, error, isIconAvailable);
getDisableStyle(theming);
getEditModeStyle(theming, deviceSizeMode, scope, isRTL, error, isIconAvailable);
getReadModeStyle(theming, deviceSizeMode, scope, isRTL, error, isControlDisabled, isIconAvailable);
getHeaderStyle(theming);
getFooterStyle(theming);
getActionIconStyle(theming, isRTL, error, isRestMode);
getActionIconWidth(theming);
getOutlineStyle(theming);
}
}
Depending on what you're trying to accomplish, you may need some or all of these. In this case, we're just going to use the edit and read styles. We'll update our getStyle function as follows:
getStyle(context) {
let parameters = context.parameters;
let themingHelper = MscrmControls.FieldControls.ThemingHelper;
let style;
let theming = context.theming;
let deviceSizeMode = parameters.deviceSizeMode;
let scope = parameters.scope;
if (this.props.active) {
style = themingHelper.getEditModeStyle(theming, deviceSizeMode, scope);
} else {
style = themingHelper.getReadModeStyle(theming, deviceSizeMode, scope);
}
let highlight = parameters.highlight.raw == "yes";
let threshold = parameters.threshold.raw;
if (highlight && this.props.value < threshold) {
style.color = parameters.highlightColor.raw;
} else {
style.color = null;
}
return style;
}
You can download the solution which uses the helper method here: CustomControls_1_0_0_3.zip.
Derived Class
Finally, the best/easiest option is to just extend one of the existing controls. As mentioned above, the Microsoft Virtual Control Library contains several classes which can help us out. In particular, it contains base classes which we can extend to build controls -- for example, VirtualNumberControl. Since we're extending a class, much of the logic is already handled, which makes the code for our control much simpler!
namespace BGuidinger.Samples {
export class PercentageControl extends MscrmControls.FieldControls.VirtualNumberControl {
private value;
getOutputs() {
return { value: this.value };
}
setControlValue(value) {
this.value = value;
}
valueForMode(mode) {
let value = this.context.parameters.value.raw;
if (this.waitingForValueSaved && this.value != value) {
return this.value;
}
if (mode.id == MscrmControls.FieldControls.ModeDescriptorID.ACTIVE) {
return value;
} else {
return value + "%";
}
}
styleForMode(mode) {
let style = super.styleForMode(mode);
let parameters = this.context.parameters;
let highlight = parameters.highlight.raw == "yes";
let threshold = parameters.threshold.raw;
if (highlight && this.value < threshold) {
style.color = parameters.highlightColor.raw;
} else {
style.color = null;
}
return style;
}
}
}
This control is almost fully functional as a percentage control! It will show with a percent sign when in read mode, and it will only show the number when in edit mode. However, it still lacks some functionality such as hiding the value if the user does not have access to it.
You can download the solution which uses inheritance here: CustomControls_1_0_0_4.zip.
Comments
Bob, thanks for sharing. That's incredible! You've doing great work I had no time for but I plan to catch up during next 2-3 weeks.
Thanks, man! Appreciate it. I have a few more posts planned on custom controls, but I want to spend some more time playing around with it before posting.
Bob, really it's amazing to see first person who spends valuable time to use this feature even before Microsoft does.. hats off you bro.. Will be keep following you.. you are rocking.
Really awesome! Good Job, will share it tomorrow with my colleagues :)
Hi Bob,
Thanks for this article. If you get some time to share custom control for Option set show different images based on value. Like Microsoft introduce adding images in views/grids.
This is incredible. Simply superb...
Hi Bob,
thanks for your guidance in the ccf-jungle.
Do you have a hint for me?
I always have to clear all occurrences of the new custom control and remove it completely, if I wanna test the next version of the custom control. If I don't remove it, it won't get updated.
(And I am publishing all customizations!)
Hi Christian,
Good question, I am experiencing the same behavior. To speed up my developments (and overcome the issue) I am using Fiddler AutoResponder (check: https://docs.microsoft.com/en-us/dynamics365/customer-engagement/developer/streamline-javascript-development-fiddler-autoresponder).
Hope it helps and I am also looking forward to the right process/solution to this issue.
Regards
Jonas Wauters
Hi Bob,
Is the source of the msvcontrollib-1.0.0.js available somewhere? I found a bug: when focusing a field it selects all text, then when clicking the form, the content is gone. So, I wanted to fix that inside msvcontrollib.
Best regards,
Niels
Hi Bob
Great articles!
Would you know if this msvcontrollib allows/will allow to extend a grid too?
Don't seem to find it at this moment in msvcontrollib
Kind regards
Kim