feat(aio): add attribute utils for code atty interpretation.

These utils support flexible, natural attribute interpretation as applied to code-example and code-pane. Then apply those utils to code-example and live-example
This commit is contained in:
Ward Bell 2017-05-02 19:17:05 -07:00 committed by Matias Niemelä
parent 8760bf7be4
commit 673d8ae583
4 changed files with 208 additions and 26 deletions

View File

@ -1,5 +1,6 @@
/* tslint:disable component-selector */
import { Component, ElementRef, OnInit } from '@angular/core';
import { getBoolFromAttribute } from 'app/shared/attribute-utils';
/**
* An embeddable code block that displays nicely formatted code.
@ -38,7 +39,7 @@ export class CodeExampleComponent implements OnInit {
this.path = element.getAttribute('path') || '';
this.region = element.getAttribute('region') || '';
this.title = element.getAttribute('title') || '';
this.hideCopy = element.getAttribute('hideCopy') === 'true';
this.hideCopy = getBoolFromAttribute(element, ['hidecopy', 'hide-copy']);
}
ngOnInit() {

View File

@ -2,6 +2,8 @@
import { Component, ElementRef, HostListener, Input, OnInit, AfterViewInit, ViewChild } from '@angular/core';
import { Location } from '@angular/common';
import { boolFromValue, getAttrs, getAttrValue } from 'app/shared/attribute-utils';
const defaultPlnkrImg = 'plunker/placeholder.png';
const imageBase = 'content/images/';
const liveExampleBase = 'content/live-examples/';
@ -87,7 +89,7 @@ export class LiveExampleComponent implements OnInit {
private elementRef: ElementRef,
location: Location ) {
const attrs = this.attrs = this.getAttrs();
const attrs = this.attrs = getAttrs(this.elementRef);
let exampleDir = attrs.name;
if (!exampleDir) {
// take last segment, excluding hash fragment and query params
@ -98,12 +100,11 @@ export class LiveExampleComponent implements OnInit {
this.plnkrName = attrs.plnkr ? attrs.plnkr.trim() + '.' : '';
this.zip = `${zipBase}${exampleDir}/${this.plnkrName}${this.zipName}.zip`;
const noDownload = this.getAttrValue(['noDownload', 'nodownload']); // noDownload aliases
this.enableDownload = !boolFromAtty(noDownload);
this.enableDownload = !boolFromValue(getAttrValue(attrs, 'nodownload'));
this.plnkrImg = imageBase + (attrs.img || defaultPlnkrImg);
if (boolFromAtty(this.getAttrValue(['downloadOnly', 'downloadonly']))) {
if (boolFromValue(getAttrValue(attrs, 'downloadonly'))) {
this.mode = 'downloadOnly';
}
}
@ -116,7 +117,7 @@ export class LiveExampleComponent implements OnInit {
let plnkrStyle = 'eplnkr'; // embedded style by default
this.mode = 'default'; // display in another browser tab by default
this.isEmbedded = boolFromAtty(attrs.embedded);
this.isEmbedded = boolFromValue(attrs.embedded);
if (this.isEmbedded) {
this.mode = 'embedded'; // display embedded in the doc
@ -126,11 +127,11 @@ export class LiveExampleComponent implements OnInit {
// If wide enough, choose style based on style attributes
if (width > this.narrowWidth) {
// Make flat style with `flat-style` or `embedded-style="false`; support atty aliases
const flatStyle = this.getAttrValue(['flat-style', 'flatstyle', 'flatStyle']);
const isFlatStyle = boolFromAtty(flatStyle);
const flatStyle = getAttrValue(attrs, ['flat-style', 'flatstyle']);
const isFlatStyle = boolFromValue(flatStyle);
const embeddedStyle = this.getAttrValue(['embedded-style', 'embeddedstyle', 'embeddedStyle']);
const isEmbeddedStyle = boolFromAtty(embeddedStyle, !isFlatStyle);
const embeddedStyle = getAttrValue(attrs, ['embedded-style', 'embeddedstyle']);
const isEmbeddedStyle = boolFromValue(embeddedStyle, !isFlatStyle);
plnkrStyle = isEmbeddedStyle ? 'eplnkr' : 'plnkr';
}
}
@ -138,17 +139,6 @@ export class LiveExampleComponent implements OnInit {
this.plnkr = `${liveExampleBase}${exampleDir}/${this.plnkrName}${plnkrStyle}.html`;
}
getAttrs(): any {
const attrs = this.elementRef.nativeElement.attributes;
const attrMap = {};
Object.keys(attrs).forEach(key => attrMap[attrs[key].name] = attrs[key].value);
return attrMap;
}
getAttrValue(atty: string | string[]) {
return this.attrs[typeof atty === 'string' ? atty : atty.find(a => this.attrs[a] !== undefined)];
}
ngOnInit() {
// The `liveExampleContent` property is set by the DocViewer when it builds this component.
// It is the original innerHTML of the host element.
@ -168,11 +158,6 @@ export class LiveExampleComponent implements OnInit {
toggleEmbedded () { this.showEmbedded = !this.showEmbedded; }
}
function boolFromAtty(atty: string , def: boolean = false) {
// tslint:disable-next-line:triple-equals
return atty == undefined ? def : atty.trim() !== 'false';
}
///// EmbeddedPlunkerComponent ///
/**
* Hides the <iframe> so we can test LiveExampleComponent without actually triggering

View File

@ -0,0 +1,148 @@
import { ElementRef } from '@angular/core';
import { getAttrs, getAttrValue, getBoolFromAttribute, boolFromValue } from './attribute-utils';
describe('Attribute Utilities', () => {
let testEl: HTMLElement;
beforeEach(() => {
const div = document.createElement('div');
div.innerHTML = `<div a b="true" c="false" D="foo" d-E></div>`;
testEl = div.querySelector('div');
});
describe('#getAttrs', () => {
beforeEach(() => {
this.expectedMap = {
a: '',
b: 'true',
c: 'false',
d: 'foo',
'd-e': ''
};
});
it('should get attr map from getAttrs(element)', () => {
const actual = getAttrs(testEl);
expect(actual).toEqual(this.expectedMap);
});
it('should get attr map from getAttrs(elementRef)', () => {
const actual = getAttrs(new ElementRef(testEl));
expect(actual).toEqual(this.expectedMap);
});
});
describe('#getAttrValue', () => {
let attrMap: { [index: string]: string };
beforeEach(() => {
attrMap = getAttrs(testEl);
});
it('should return empty string value for attribute "a"', () => {
expect(getAttrValue(attrMap, 'a')).toBe('');
});
it('should return empty string value for attribute "A"', () => {
expect(getAttrValue(attrMap, 'a')).toBe('');
});
it('should return "true" for attribute "b"', () => {
expect(getAttrValue(attrMap, 'b')).toBe('true');
});
it('should return empty string value for attribute "d-E"', () => {
expect(getAttrValue(attrMap, 'd-e')).toBe('');
});
it('should return empty string for attribute ["d-e"]', () => {
// because d-e will be found before d
expect(getAttrValue(attrMap, ['d-e'])).toBe('');
});
it('should return "foo" for attribute ["d", "d-e"]', () => {
// because d will be found before d-e
expect(getAttrValue(attrMap, ['d', 'd-e'])).toBe('foo');
});
it('should return empty string for attribute ["d-e", "d"]', () => {
// because d-e will be found before d
expect(getAttrValue(attrMap, ['d-e', 'd'])).toBe('');
});
it('should return undefined value for non-existent attribute "x"', () => {
expect(getAttrValue(attrMap, 'x')).toBeUndefined();
});
it('should return undefined if no argument', () => {
expect(getAttrValue(attrMap)).toBeUndefined();
});
});
describe('#boolFromValue', () => {
let attrMap: { [index: string]: string };
beforeEach(() => {
attrMap = getAttrs(testEl);
});
it('should return true for present but unassigned attr "a"', () => {
expect(boolFromValue(getAttrValue(attrMap, 'a'))).toBe(true);
});
it('should return true for attr "b" which is "true"', () => {
expect(boolFromValue(getAttrValue(attrMap, 'b'))).toBe(true);
});
it('should return false for attr "c" which is "false"', () => {
expect(boolFromValue(getAttrValue(attrMap, 'c'))).toBe(false);
});
it('should return false by default for undefined attr "x"', () => {
expect(boolFromValue(getAttrValue(attrMap, 'x'))).toBe(false);
});
it('should return true for undefined attr "x" when default is true', () => {
const value = getAttrValue(attrMap, 'x');
expect(boolFromValue(value, true)).toBe(true);
});
it('should return false for undefined attr "x" when default is false', () => {
const value = getAttrValue(attrMap, 'x');
expect(boolFromValue(value, false)).toBe(false);
});
it('should return true for present but unassigned attr "a" even when default is false', () => {
// default value is only applied when the attribute is missing
const value = getAttrValue(attrMap, 'a');
expect(boolFromValue(value, false)).toBe(true);
});
});
// Combines the three utilities for convenience.
describe('#getBoolFromAttribute', () => {
it('should return true for present but unassigned attr "a"', () => {
expect(getBoolFromAttribute(testEl, 'a')).toBe(true);
});
it('should return true for attr "b" which is "true"', () => {
expect(getBoolFromAttribute(testEl, 'b')).toBe(true);
});
it('should return false for attr "c" which is "false"', () => {
expect(getBoolFromAttribute(testEl, 'c')).toBe(false);
});
it('should return true for attributes ["d-e", "d"]', () => {
// because d-e will be found before D="foo"
expect(getBoolFromAttribute(testEl, ['d-e', 'd'])).toBe(true);
});
it('should return false for non-existent attribute "x"', () => {
expect(getBoolFromAttribute(testEl, 'x')).toBe(false);
});
});
});

View File

@ -0,0 +1,48 @@
// Utilities for processing HTML element attributes
import { ElementRef } from '@angular/core';
interface StringMap { [index: string]: string; }
/**
* Get attribute map from element or ElementRef `attributes`.
* Attribute map keys are forced lowercase for case-insensitive lookup.
* @param el The source of the attributes
*/
export function getAttrs(el: HTMLElement | ElementRef): StringMap {
const attrs: NamedNodeMap = el instanceof ElementRef ? el.nativeElement.attributes : el.attributes;
const attrMap = {};
Object.keys(attrs).forEach(key =>
attrMap[(attrs[key] as Attr).name.toLowerCase()] = (attrs[key] as Attr).value);
return attrMap;
}
/**
* Return the attribute that matches `atty`.
* @param atty Name of the attribute or a string of candidate attribute names
*/
export function getAttrValue(attrs: StringMap, attr: string | string[] = ''): string {
return attrs[typeof attr === 'string' ? attr : attr.find(a => attrs[a] !== undefined)];
}
/**
* Return the boolean state of an attribute value (if supplied)
* @param attyValue The string value of some attribute (or undefined if attribute not present)
* @param def Default boolean value when attribute is undefined.
*/
export function boolFromValue(attrValue: string, def: boolean = false) {
// tslint:disable-next-line:triple-equals
return attrValue == undefined ? def : attrValue.trim() !== 'false';
}
/**
* Return the boolean state of attribute from an element
* @param el The source of the attributes
* @param atty Name of the attribute or a string of candidate attribute names
* @param def Default boolean value when attribute is undefined.
*/
export function getBoolFromAttribute(
el: HTMLElement | ElementRef,
attr: string | string[],
def: boolean = false): boolean {
return boolFromValue(getAttrValue(getAttrs(el), attr), def);
}