Glass Mapper RenderLink link description - default text if empty - sitecore

<li>#if (string.IsNullOrWhiteSpace(topLinks.Target.Text))
{
topLinks.Target.Text = "EMPTY DESCRIPTION";
}
#(RenderLink(topLinks, x => x.Target, isEditable: true))
</li>
I need a way to catch it when a Content Editor has set up a link, but not actually put a Link Description in. At the moment it just renders spaces. The above works, but it's clunky and I need to put it everywhere I use a RenderLink. How do I default the text if it's empty?

I've created an extension method to work around it.
Note that I've extended GlassHtml and not GlassView because you may want to pass a different model type than the one that's used for the view.
namespace ParTech.MvcDemo.Context.Extensions
{
using System;
using System.Linq.Expressions;
using System.Web;
using Glass.Mapper.Sc;
using Glass.Mapper.Sc.Fields;
public static class GlassHtmlExtensions
{
public static HtmlString RenderLinkWithDefaultText<T>(this GlassHtml glassHtml, T model, Expression<Func<T, object>> field, object attributes = null, bool isEditable = true, string defaultText = null)
{
var linkField = field.Compile().Invoke(model) as Link;
if (linkField == null || string.IsNullOrEmpty(linkField.Text))
{
return new HtmlString(glassHtml.RenderLink(model, field, attributes, isEditable, defaultText));
}
return new HtmlString(glassHtml.RenderLink(model, field, attributes, isEditable));
}
}
}
You can now do this in your view:
#(((GlassHtml)this.GlassHtml).RenderLinkWithDefaultText(MyModel, x => x.LinkField, null, true, "Static default text"))
Still a bit hacky because you need to cast the IGlassHtml to GlassHtml, but it works.
If you always have the correct model defined for you view (and thus don't need to specify the model parameter) you could put this extension method on GlassView.

Related

Hook to item selection action

When I select an item I want to check some fields before it will be displayed in the Field Editor and then change values on other fields.
So I need to subscribe to an event, but such event doesn't exist out of the box as I can see. Is there a way to hook to item selection action or I need to create a custom event, if so - where do I need to raise it?
Sounds like you need to create a custom validator - this blog post describes the process:
https://www.habaneroconsulting.com/stories/insights/2016/creating-a-custom-field-validator-in-sitecore
In summary:
Create a new field rule (Field validators are located in /sitecore/system/Settings/Validation Rules/Field Rules/) linking to your assembly. The blog post above gives the following example of a field validator
[Serializable]
namespace MySitecore.Project.Validators
{
// This validator ensures that the description attribute of a link is specified
public class LinkTextValidator : StandardValidator
{
public override string Name
{
get { return "Link text validator"; }
}
public LinkTextValidator() {}
public LinkTextValidator(SerializationInfo info, StreamingContext context) : base(info, context) { }
protected override ValidatorResult Evaluate()
{
Field field = this.GetField();
if (field == null)
return ValidatorResult.Valid;
string str1 = this.ControlValidationValue;
if (string.IsNullOrEmpty(str1) || string.Compare(str1, "<link>", StringComparison.InvariantCulture) == 0)
return ValidatorResult.Valid;
XmlValue xmlValue = new XmlValue(str1, "link");
string attribute = xmlValue.GetAttribute("text");
if (!string.IsNullOrEmpty(xmlValue.GetAttribute("text")))
return ValidatorResult.Valid;
this.Text = this.GetText("Description is missing in the link field \"{0}\".", field.DisplayName);
// return the failed result value defined in the parameters for this validator; if no Result parameter
// is defined, the default value FatalError will be used
return this.GetFailedResult(ValidatorResult.CriticalError);
}
protected override ValidatorResult GetMaxValidatorResult()
{
return this.GetFailedResult(ValidatorResult.Error);
}
}
}
Credit to: MICHAEL ARMSTRONG

Email regex not allowing null value

I'm using Angular and using this validator
public static validate(c: AbstractControl) {
const EMAIL_REGEXP = /^(|(([A-Za-z0-9]+_+)|([A-Za-z0-9]+\-+)|([A-Za-z0-9]+\.+)|([A-Za-z0-9]+\++))*[A-Za-z0-9]+#((\w+\-+)|(\w+\.))*\w{1,63}\.[a-zA-Z]{2,6})$/i;
return EMAIL_REGEXP.test(c.value) ? null : {
validateEmail: {
valid: false
}
};
}
It does not allow me to have an empty value in input field.
What am I doing wrong?
Unless you have specific requirements, you can simply use the e-mail validator that Angular provides.
mailControl: FormControl = new FormControl('', [Validators.email]);
If you don't add a required validator, you will have the option to let it empty.

CSOM Field.FromBaseType returns false, although the field is derived from another content type

I retrieve the FieldCollection of a content type via client side object model:
var fields = contentType.Fields;
clientContext.Load(fields);
clientContext.ExecuteQuery();
Then I cycle through the fields and check if the field is derived:
if (field.FromBaseType) { ... }
This works for the field "Title" which is derived from "Item", but not for fields the content type has derived from another custom content type.
Why is FromBaseType true for the "Title" field, but not for the fields of the direct parent content type? And how can I find out, if a field is derived?
SPField.FromBaseType property gets a Boolean value that indicates whether the field derives from a base field type and it is not the same as determining whether the field derives from parent content type.
The following method demonstrates how o determine the source content type of a field:
public static ContentType GetSource(Field field, ContentType contentType)
{
var ctx = field.Context;
var parentCt = contentType.Parent;
ctx.Load(parentCt);
ctx.Load(parentCt.FieldLinks);
ctx.ExecuteQuery();
var fieldLink = parentCt.FieldLinks.FirstOrDefault(fl => fl.Name == field.InternalName);
if (parentCt.StringId != "0x01" && fieldLink != null)
{
return GetSource(field, parentCt);
}
return (fieldLink == null ? contentType : parentCt);
}
Usage
var fields = contentType.Fields;
ctx.Load(fields);
ctx.ExecuteQuery();
foreach (var field in fields)
{
var source = GetSource(field, contentType);
Console.WriteLine("Field: {0} Source: {1}",field.Title,source.Name);
}
As Vadim said, the Field.FromBaseType property does only indicate if the field derives from a base field type. It says nothing about the content type.
So, I solved my problem by loading the fields of the parent content type, too and checking if they contain a field with the same ID:
var fields = contentType.Fields;
var parentFields = contentType.Parent.Fields;
clientContext.Load(fields);
clientContext.Load(parentFields);
clientContext.ExecuteQuery();
foreach (var field in fields)
{
if (Enumerable.Any(parentFields, pf => pf.Id == field.Id))
{
// ...
}
}
But this is kind of crude ... if someone has a better answer I would be happy to accept it.

How to enable VersionCountDisabler for Glass Mapper in Sitecore for SitecoreQuery and SitecoreChildren attributes

The glass mapper will return null object or (no items) for SitecoreQuery and SitecoreChildren attribute that are placed on the GlassModels. These attributes don't take any such parameter where I can specify them to return items if they don't exist in the the context lanaguge. The items e.g. exist in EN but don't exist in en-ES. I need to put a lot of null check in my views to avoid Null exception and makes the views or controller very messy. It is lot of boiler plate code that one has to write to make it work.
In Page Editor the SitecoreChildren returns item and content authors can create items in that langauge version by editing any field on the item. This automatically creates the item in that langauge. However the same code will fail in Preview mode as SitecoreChidren will return null and you see null pointer exception.
SitecoreQuery doesn't return any items in page editor and then Content Authors wont be able to create items in Page editor.
To make the experience good if we can pass a parameter to SiteocreQuery attribute so it disable VsersionCount and returns the items if they dont exist in that langauge.
This is actually not possible. There is an issue on GitHub which would make it easy to create a custom attribute to handle this very easy. Currently you need to create a new type mapper and copy all the code from the SitecoreQueryMapper. I have written a blog post here about how you can create a custom type mapper. You need to create the following classes (example for the SitecoreQuery).
New configuration:
public class SitecoreSharedQueryConfiguration : SitecoreQueryConfiguration
{
}
New attribute:
public class SitecoreSharedQueryAttribute : SitecoreQueryAttribute
{
public SitecoreSharedQueryAttribute(string query) : base(query)
{
}
public override AbstractPropertyConfiguration Configure(PropertyInfo propertyInfo)
{
var config = new SitecoreSharedQueryConfiguration();
this.Configure(propertyInfo, config);
return config;
}
}
New type mapper:
public class SitecoreSharedQueryTypeMapper : SitecoreQueryMapper
{
public SitecoreSharedQueryTypeMapper(IEnumerable<ISitecoreQueryParameter> parameters)
: base(parameters)
{
}
public override object MapToProperty(AbstractDataMappingContext mappingContext)
{
var scConfig = Configuration as SitecoreQueryConfiguration;
var scContext = mappingContext as SitecoreDataMappingContext;
using (new VersionCountDisabler())
{
if (scConfig != null && scContext != null)
{
string query = this.ParseQuery(scConfig.Query, scContext.Item);
if (scConfig.PropertyInfo.PropertyType.IsGenericType)
{
Type outerType = Glass.Mapper.Sc.Utilities.GetGenericOuter(scConfig.PropertyInfo.PropertyType);
if (typeof(IEnumerable<>) == outerType)
{
Type genericType = Utilities.GetGenericArgument(scConfig.PropertyInfo.PropertyType);
Func<IEnumerable<Item>> getItems;
if (scConfig.IsRelative)
{
getItems = () =>
{
try
{
return scContext.Item.Axes.SelectItems(query);
}
catch (Exception ex)
{
throw new MapperException("Failed to perform query {0}".Formatted(query), ex);
}
};
}
else
{
getItems = () =>
{
if (scConfig.UseQueryContext)
{
var conQuery = new Query(query);
var queryContext = new QueryContext(scContext.Item.Database.DataManager);
object obj = conQuery.Execute(queryContext);
var contextArray = obj as QueryContext[];
var context = obj as QueryContext;
if (contextArray == null)
contextArray = new[] { context };
return contextArray.Select(x => scContext.Item.Database.GetItem(x.ID));
}
return scContext.Item.Database.SelectItems(query);
};
}
return Glass.Mapper.Sc.Utilities.CreateGenericType(typeof(ItemEnumerable<>), new[] { genericType }, getItems, scConfig.IsLazy, scConfig.InferType, scContext.Service);
}
throw new NotSupportedException("Generic type not supported {0}. Must be IEnumerable<>.".Formatted(outerType.FullName));
}
{
Item result;
if (scConfig.IsRelative)
{
result = scContext.Item.Axes.SelectSingleItem(query);
}
else
{
result = scContext.Item.Database.SelectSingleItem(query);
}
return scContext.Service.CreateType(scConfig.PropertyInfo.PropertyType, result, scConfig.IsLazy, scConfig.InferType, null);
}
}
}
return null;
}
public override bool CanHandle(AbstractPropertyConfiguration configuration, Context context)
{
return configuration is SitecoreSharedQueryConfiguration;
}
}
And configure the new type mapper in your glass config (mapper and parameters for the constructor):
container.Register(Component.For<AbstractDataMapper>().ImplementedBy<SitecoreSharedQueryTypeMapper>().LifeStyle.Transient);
container.Register(Component.For<IEnumerable<ISitecoreQueryParameter>>().ImplementedBy<List<ItemPathParameter>>().LifeStyle.Transient);
container.Register(Component.For<IEnumerable<ISitecoreQueryParameter>>().ImplementedBy<List<ItemIdParameter>>().LifeStyle.Transient);
container.Register(Component.For<IEnumerable<ISitecoreQueryParameter>>().ImplementedBy<List<ItemIdNoBracketsParameter>>().LifeStyle.Transient);
container.Register(Component.For<IEnumerable<ISitecoreQueryParameter>>().ImplementedBy<List<ItemEscapedPathParameter>>().LifeStyle.Transient);
container.Register(Component.For<IEnumerable<ISitecoreQueryParameter>>().ImplementedBy<List<ItemDateNowParameter>>().LifeStyle.Transient);
You can then simply change the SitecoreQuery attribute on your model to SitecoreSharedQuery:
[SitecoreSharedQuery("./*")]
public virtual IEnumerable<YourModel> YourItems { get; set; }
For the children you could either use the shared query mapper and querying the children or create the same classes for a new SitecoreSharedChildren query.
Edit: Added bindings for IEnumerable<ISitecoreQueryParameter> as they are missing and therefor it threw an error.

Automatic type casting of vanilla objects to Ember objects

I'm just diving in to Ember. I'm looking for a way to pass a plain array of vanilla objects into a collection/controller and have them type cast to the correct model.
Here's the simple collection view:
{{#collection id="prods" contentBinding="Vix.prodsController" tagName="ul"}}
{{content.title}}
{{/collection}}
Here's the model:
Vix.Prod = Ember.Object.extend({
id: null,
title: null
});
And the controller:
Vix.prodsController = Ember.ArrayController.create({
content: []
});
Then let's get some JSON-formatted data from the server. In this example I'll just hard-code it:
var prods = [{id:"yermom1", title:"yermom 1"}, {id:"yermom2", title:"yermom 2"}]
Vix.prodsController.set('content', prods);
So far so good. I get my simple list of li elements displaying the titles as I'd expect. But when I want to update the title of one of the objects, using:
Vix.prodsController.objectAt(0).set('title', 'new title')
It complains because the object has no set method-- it has not been properly cast to my Vix.Prod Ember Object.
Using this alternative:
Vix.prodsController.pushObjects(prods);
Produces the same result. It's only if I explicitly create new model instances that I get the get/set goodness:
var prods = [Vix.Prod.create({id:"yermom1", title:"yermom 1"}), {Vix.Prod.create(id:"yermom2", title:"yermom 2"})]
Is there a way to automatically type cast those vanilla objects to my Vix.Prod Ember Object? If not, am I the only one that really wants something like that? In Backbone one can set the model property on a collection. I suppose I can create a setter on my Controller to do something similar- just wondering if there is something built-in that I'm missing. Thanks!
No magic. I'd suggest do a loop wrapping the model.
var prods = [{id:"yermom1", title:"yermom 1"}, {id:"yermom2", title:"yermom 2"}];
for (var i = 0; i < prods.length; i++) {
prods[i] = Vix.Prod.create(prods[i]);
}
If I use ember as much as I hope to, I'm going to want a shortcut. So here's what I've done for now. I created a base Collection class that I use to create my Collections/Controllers:
Vix.Collection = Ember.ArrayController.extend({
model: null,
pushObject: function(obj) {
if (this.get('model') && obj.__proto__.constructor !== this.get('model')) {
obj = this.get('model').create(obj);
}
return this._super(obj);
},
pushObjects: function(objs) {
if (this.get('model')) {
objs = this._typecastArray(objs)
}
return this._super(objs);
},
set: function(prop, val) {
if (prop === 'content' && this.get('model')) {
val = this._typecastArray(val);
}
return this._super(prop, val);
},
_typecastArray: function(objs) {
var typecasted = [];
objs.forEach(function(obj){
if (obj.__proto__.constructor !== this.get('model')) {
obj = this.get('model').create(obj);
}
typecasted.push(obj);
}, this);
return typecasted;
}
})
Now when I call pushObject, pushObjects, or .set('collection', data), if the collection instance has a defined model property and the objects being added to the collection aren't already of that type, they'll be cast. Been working good so far, but I welcome any feedback.
You should have a look at ember-data: https://github.com/emberjs/data
It seems to fit your needs...
As of today, it's not yet production ready (as stated in the readme), but is quickly converging toward maturity, thanks to an active development.