Skip to content

Commit

Permalink
Fixes url slug (link) resolving #17
Browse files Browse the repository at this point in the history
  • Loading branch information
Enngage committed Sep 15, 2017
1 parent 31e4770 commit 39e0ee1
Show file tree
Hide file tree
Showing 20 changed files with 271 additions and 143 deletions.
44 changes: 26 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -262,53 +262,61 @@ deliveryClient.items<Movie>()
.subscribe(response => console.log(response));
```

### URL Slugs
### URL Slugs (links)

URL slugs enable you to generate user friendly URLs while giving editors the capability to control the looks of it. As a developer you will need to take the URL slug defined by editors and convert it to a path that your application knows and can render. URL slugs can be resolved either globally or locally for each query.
Url slugs (links) can be resolved in either `URLSlugField` or `RichTextField` fields. The way how links are resolved depends on the `linkResolver` which can be defined either globally in model definition, or passed it through the `IQueryConfig` of a particular api call. The query resolver has priority over the globally defined one.

For example, if you define a URL slug for your item as `dwayne-johnson` and your application is able to handle requests such as `yourApp.com/actors/{urlSlug}`, you will need to configure the `urlSlugResolver` of your `ContentItem` class to resolve such item. This example would transfer to the code below.
To access the url call `getUrl` method.

#### Resolving URL slugs globally
Please note that when resolving links in RichTextField, you resolve all of them with a single link resolver. For this reason it is recommended that you specify the `type` of the content type you want to resolve.

#### Resolving URL slugs (links) globally

```typescript
import { ContentItem, Fields } from 'kentico-cloud-delivery-typescript-sdk';
import { ContentItem, Fields, ILink } from 'kentico-cloud-delivery-typescript-sdk';

export class Actor extends ContentItem {
public title: Fields.TextField;
public slug: Fields.UrlSlugField;

constructor() {
super({
urlSlugResolver: (contentItem: IContentItem, urlSlug: string) => {
// you can also access additional content data using the `contentItem` property
return `/actors/${urlSlug}`;
linkResolver: (link: ILink) => {
if (link.type === 'actor'){
return `/actors/${urlSlug}`;
}
return 'unkown-type-link';
}
})
}
}
```

To get the URL, access the `url` property of your `UrlslugField`:

```typescript
deliveryClient.item<Actor>('actorCodename')
.get()
.subscribe(response => console.log(response.item.slug.url));
.subscribe(response => console.log(response.item.slug.getUrl()));
```

#### Resolving URL slugs locally

Additionally, you can specify a URL slug resolver when getting content items using the `queryConfig` method. Setting the URL slug resolver this way has priority over the one defined in a model.
#### Resolving URL slugs (links) per query

```typescript
import { ContentItem, Fields, ILink } from 'kentico-cloud-delivery-typescript-sdk';

deliveryClient.item<Actor>('actorCodename')
.queryConfig({
urlSlugResolver: (contentItem: IContentItem, urlSlug: string) => {
return `/actors/${urlSlug}`;
}
linkResolver: (link: ILink) => {
if (link.type === 'actor'){
return `/actors/${urlSlug}`;
}
else if (link.type === 'movie'){
return `/movies/${urlSlug}`;
}
return 'unkown-type-link';
}
})
.get()
.subscribe(response => console.log(response.item.slug.url));
.subscribe(response => console.log(response.item.slug.getUrl()));
```

### Resolving modular content in Rich text fields
Expand Down
14 changes: 13 additions & 1 deletion lib/fields/field-interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ export namespace FieldInterfaces {
name: string;
type: FieldType;
value: any;
modular_content?: string[];
taxonomy_group?: string;
}

Expand All @@ -27,4 +26,17 @@ export namespace FieldInterfaces {
name: string;
codename: string;
}

export interface IRichTextField extends IField {

/**
* Modular content items
*/
modular_content?: string[];

/**
* Json with links identified by item Id
*/
links: any;
}
}
44 changes: 28 additions & 16 deletions lib/fields/field-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@ import { FieldType } from './field-type';
import { IContentItem } from '../interfaces/item/icontent-item.interface';
import { FieldInterfaces } from './field-interfaces';
import { FieldModels } from './field-models';
import { ILink } from '../interfaces/item/ilink.interface';
import { Link } from '../models/item/link.class';
import { RichTextResolver } from './rich-text-resolver.class';
import { IItemQueryConfig } from '../interfaces/item/iitem-query.config';
import { TypeResolverService } from '../services/type-resolver.service';

export namespace Fields {

Expand Down Expand Up @@ -118,13 +121,18 @@ export namespace Fields {
* @constructor
* @param {string} name - Name of the field
* @param {string} value - Value of the field
* @param {IContentItem[]} modularItems - Modular items
* @param {ILink[]} links - Links in rich text field
* @param {TypeResolverService} typeResolverService - Type resolver service
* @param {boolean} enableAdvancedLogging - Indicates if advanced issues are logged in console
* @param {IItemQueryConfig} itemQueryConfig - Item query config
*/
constructor(
public name: string,
public value: any,
public modularItems: IContentItem[],
public links: ILink[],
public typeResolverService: TypeResolverService,
public enableAdvancedLogging: boolean,
public itemQueryConfig: IItemQueryConfig
) {
Expand All @@ -137,7 +145,7 @@ export namespace Fields {
return this.resolvedHtml;
}

var richTextHelper = new RichTextResolver(this.value, this.modularItems, this.enableAdvancedLogging, this.itemQueryConfig)
var richTextHelper = new RichTextResolver(this.value, this.modularItems, this.links, this.typeResolverService, this.enableAdvancedLogging, this.itemQueryConfig)
this.resolvedHtml = richTextHelper.resolveHtml();

return this.resolvedHtml;
Expand Down Expand Up @@ -216,43 +224,47 @@ export namespace Fields {
*/
public type: FieldType = FieldType.url_slug;

/**
* Resolved Url of the item
*/
public url: string;

/**
* Represents URL slug field of Kentico Cloud item
* @constructor
* @param {string} name - Name of the field
* @param {string} value - Value of the field
* @param {string} contentItemType - Type of the content item
* @param {((contentItem: IContentItem, urlSlug: string) => string) | undefined} urlSlugResolver - Callback used to resolve URL slug of the item with type
* @param {IContentItem} item - Content item, required in order to get link object used by resolver
* @param {((link: ILink) => string) | undefined} linkResolver - Callback used to resolve links
* @param {boolean} enableAdvancedLogging - Indicates if advanced issues are logged in console
*/
constructor(
public name: string,
public value: string,
public contentItem: IContentItem,
public urlSlugResolver: ((contentItem: IContentItem, urlSlug: string) => string) | undefined,
public item: IContentItem,
public linkResolver: ((link: ILink) => string) | undefined,
public enableAdvancedLogging: boolean
) {
this.url = this.getUrl();
};

private getUrl(): string {
if (this.urlSlugResolver == null) {
getUrl(): string {
if (this.linkResolver == null) {
if (this.enableAdvancedLogging) {
console.log(`You have to implement 'urlSlugResolver' in your Model class or your query in order to get url of this item`);
console.log(`You have to implement 'linkResolver' in your Model class or your query in order to get url of this item`);
}
return '';
}

if (!this.item){
if (this.enableAdvancedLogging){
console.log(`Cannot resolve link for type '${this.type}' because source item is not valid`);
}
}

var url = this.urlSlugResolver(this.contentItem, this.value);
var url = this.linkResolver(new Link(
this.item.system.id,
this.item.system.codename,
this.item.system.type,
this.value
));

if (!url && this.enableAdvancedLogging) {
console.log(`'urlSlugResolver' is configured, but url resolved for '${this.contentItem.system.type}' type was resolved to empty string`);
console.log(`'linkResolver' is configured, but url resolved for '${this.type}' type was resolved to empty string`);
}

return url;
Expand Down
38 changes: 0 additions & 38 deletions lib/fields/field-utilities.ts

This file was deleted.

54 changes: 35 additions & 19 deletions lib/fields/rich-text-resolver.class.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import { IContentItem } from '../interfaces/item/icontent-item.interface';
import { FieldModels } from './field-models';
import { Fields } from './field-types';
import { IItemQueryConfig } from '../interfaces/item/iitem-query.config';
import { FieldUtilities } from './field-utilities';
import { ILink } from '../interfaces/item/ilink.interface';
import { TypeResolverService } from '../services/type-resolver.service';

export class RichTextResolver {

Expand Down Expand Up @@ -34,26 +35,24 @@ export class RichTextResolver {
*/
private readonly linkContentItemIdAttributeName = 'data-item-id';

/**
* Field utilities
*/
private fieldUtilities: FieldUtilities;

/**
* Rich text resolver
* @constructor
* @param {string} html - html to resolve
* @param {IContentItem} modularItems - modular items
* @param {ILink[]} links - links
* @param {TypeResolverService} typeResolverService - Type resolver service used to access globally defined properties of models
* @param {boolean} enableAdvancedLogging - Indicates if advanced issues are logged in console
* @param {IItemQueryConfig} queryConfig - Query configuration
*/
constructor(
private html: string,
private modularItems: IContentItem[],
private links: ILink[],
private typeResolverService: TypeResolverService,
private enableAdvancedLogging: boolean,
private queryConfig: IItemQueryConfig,
) {
this.fieldUtilities = new FieldUtilities();
};

/**
Expand Down Expand Up @@ -116,28 +115,45 @@ export class RichTextResolver {
// get id of content item
var contentItemId = contentItemIdAttribute.value;

// get content item from modular content
var contentItem = this.modularItems.find(m => m.system.id === contentItemId);
// find link with the id of content item
var link = this.links.find(m => m.itemId === contentItemId);

if (!contentItem) {
if (this.enableAdvancedLogging) {
console.log(`Could not resolve link for item with id '${contentItemId}' because no such item was found in modular items`)
if (!link){
if (this.enableAdvancedLogging){
console.warn(`Cannot resolve URL for item '${contentItemId}' because no link with this id was found`);
}
return;
}

// get url slug property
var urlSlugField = this.fieldUtilities.getUrlSlugProperty(contentItem);
if (!urlSlugField) {
// if url slug field is not defined on content type, don't process it
return;
// try to resolve link using the resolver passed through the query config
var queryLinkResolver = this.queryConfig.linkResolver;

var url;

if (queryLinkResolver){
// try to resolve url using the query config
url = queryLinkResolver(link);
}

var url = urlSlugField.url;
if (!url){
// url was not resolved, try to find global resolver for this particupar type
// and apply its url resolver

var emptyTypeItem = this.typeResolverService.createEmptyTypedObj<IContentItem>(link.type);

if (!emptyTypeItem){
throw Error(`Cannot resolve link for '${link.type}' type because mapping of this type failed`);
}

var globalLinkResolver = emptyTypeItem.linkResolver;
if (globalLinkResolver){
url = globalLinkResolver(link);
}
}

if (!url) {
if (this.enableAdvancedLogging) {
console.log(`Url for content type '${contentItem.system.type}' with id '${contentItem.system.id}' resolved to null`);
console.log(`Url for content type '${link.type}' with id '${link.itemId}' resolved to null`);
}
return;
}
Expand Down
3 changes: 3 additions & 0 deletions lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export * from './fields/field-types';
export * from './models/common/sort-order.enum';
export * from './fields/field-models';
export * from './models/common/cloud-error.class';
export * from './services/type-resolver.service';

// items
export * from './models/item/type-resolver.class';
Expand All @@ -21,6 +22,8 @@ export * from './interfaces/item/icontent-item-system-attributes.interface';
export * from './models/item/content-item-system-attributes';
export * from './query/item/multiple-item-query.class';
export * from './query/item/single-item-query.class';
export * from './interfaces/item/ilink.interface';
export * from './models/item/link.class';

// type
export * from './models/type/content-type.class';
Expand Down
3 changes: 2 additions & 1 deletion lib/interfaces/item/icontent-item.interface.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { IContentItemSystemAttributes } from './icontent-item-system-attributes.interface';
import { IContentItem } from './icontent-item.interface';
import { ILink } from '../../interfaces/item/ilink.interface';

export interface IContentItem {
system: IContentItemSystemAttributes;
elements: any;

propertyResolver?: (fieldName: string) => string;
urlSlugResolver?: (contentItem: IContentItem, urlSlug: string) => string;
linkResolver?: (link: ILink) => string;
richTextResolver?: (contentItem: IContentItem) => string;
}

3 changes: 2 additions & 1 deletion lib/interfaces/item/iitem-query.config.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { IContentItem } from './icontent-item.interface';
import { ILink } from './ilink.interface';

export interface IItemQueryConfig {
urlSlugResolver?: (contentItem: IContentItem, urlSlug: string) => string;
linkResolver?: (link: ILink) => string;
usePreviewMode?: boolean;
richTextResolver?: (contentItem: IContentItem) => string;
}
6 changes: 6 additions & 0 deletions lib/interfaces/item/ilink.interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export interface ILink {
itemId: string;
codename: string;
type: string;
url_slug: string;
}
Loading

0 comments on commit 39e0ee1

Please sign in to comment.