diff --git a/apps/nativescript-demo-ng/src/tests/spans.spec.ts b/apps/nativescript-demo-ng/src/tests/spans.spec.ts new file mode 100644 index 0000000..b1d23a8 --- /dev/null +++ b/apps/nativescript-demo-ng/src/tests/spans.spec.ts @@ -0,0 +1,136 @@ +import { Component, ElementRef, NO_ERRORS_SCHEMA, ViewChild } from '@angular/core'; +import { TestBed } from '@angular/core/testing'; +import { NativeScriptModule } from '@nativescript/angular'; +import { TextBase } from '@nativescript/core'; + +const configureComponents = (textBaseElementName: string) => { + class BaseComponent { + @ViewChild('textBase', { static: true }) textBase: ElementRef; + } + + @Component({ + template: `<${textBaseElementName} #textBase> + + + + `, + }) + class SpansComponent extends BaseComponent {} + + @Component({ + template: `<${textBaseElementName} #textBase> + + + + + + `, + }) + class FormattedStringComponent extends BaseComponent {} + + @Component({ + template: `<${textBaseElementName} #textBase> + + + + `, + }) + class DynamicSpansComponent extends BaseComponent { + show = true; + } + + @Component({ + template: `<${textBaseElementName} #textBase> + + + + + + `, + }) + class DynamicFormattedStringComponent extends BaseComponent { + show = true; + } + return { + SpansComponent, + DynamicSpansComponent, + FormattedStringComponent, + DynamicFormattedStringComponent, + }; +}; + +describe('Spans', () => { + const componentsToTest = ['Label', 'TextField', 'TextView', 'Button']; + for (const textBaseElementName of componentsToTest) { + describe(`on ${textBaseElementName}`, () => { + const { SpansComponent, DynamicSpansComponent, FormattedStringComponent, DynamicFormattedStringComponent } = configureComponents(textBaseElementName); + beforeEach(() => { + return TestBed.configureTestingModule({ + declarations: [SpansComponent, DynamicSpansComponent, FormattedStringComponent, DynamicFormattedStringComponent], + imports: [NativeScriptModule], + schemas: [NO_ERRORS_SCHEMA], + }).compileComponents(); + }); + it('correctly adds', async () => { + const fixture = TestBed.createComponent(SpansComponent); + fixture.detectChanges(); + const textBase = fixture.componentInstance.textBase.nativeElement; + expect(textBase.formattedText.spans.length).toBe(3); + expect(textBase.formattedText.spans.getItem(0).text).toBe('0'); + expect(textBase.formattedText.spans.getItem(1).text).toBe('1'); + expect(textBase.formattedText.spans.getItem(2).text).toBe('2'); + }); + it('correctly adds dynamically', async () => { + const fixture = TestBed.createComponent(DynamicSpansComponent); + const textBase = fixture.componentInstance.textBase.nativeElement; + fixture.detectChanges(); + expect(textBase.formattedText.spans.length).toBe(3); + expect(textBase.formattedText.spans.getItem(0).text).toBe('0'); + expect(textBase.formattedText.spans.getItem(1).text).toBe('1'); + expect(textBase.formattedText.spans.getItem(2).text).toBe('2'); + fixture.componentInstance.show = false; + fixture.detectChanges(); + expect(textBase.formattedText.spans.length).toBe(2); + expect(textBase.formattedText.spans.getItem(0).text).toBe('0'); + expect(textBase.formattedText.spans.getItem(1).text).toBe('2'); + fixture.componentInstance.show = true; + fixture.detectChanges(); + expect(textBase.formattedText.spans.length).toBe(3); + expect(textBase.formattedText.spans.getItem(0).text).toBe('0'); + expect(textBase.formattedText.spans.getItem(1).text).toBe('1'); + expect(textBase.formattedText.spans.getItem(2).text).toBe('2'); + }); + + it('correctly adds FormattedString', async () => { + const fixture = TestBed.createComponent(FormattedStringComponent); + fixture.detectChanges(); + const textBase = fixture.componentInstance.textBase.nativeElement; + expect(textBase.formattedText.spans.length).toBe(3); + expect(textBase.formattedText.spans.getItem(0).text).toBe('0'); + expect(textBase.formattedText.spans.getItem(1).text).toBe('1'); + expect(textBase.formattedText.spans.getItem(2).text).toBe('2'); + }); + + it('correctly adds FormattedString dynamically', async () => { + const fixture = TestBed.createComponent(DynamicFormattedStringComponent); + const textBase = fixture.componentInstance.textBase.nativeElement; + fixture.detectChanges(); + expect(textBase.formattedText.spans.length).toBe(3); + expect(textBase.formattedText.spans.getItem(0).text).toBe('0'); + expect(textBase.formattedText.spans.getItem(1).text).toBe('1'); + expect(textBase.formattedText.spans.getItem(2).text).toBe('2'); + fixture.componentInstance.show = false; + fixture.detectChanges(); + expect(textBase.formattedText.spans.length).toBe(2); + expect(textBase.formattedText.spans.getItem(0).text).toBe('0'); + expect(textBase.formattedText.spans.getItem(1).text).toBe('2'); + fixture.componentInstance.show = true; + fixture.detectChanges(); + expect(textBase.formattedText.spans.length).toBe(3); + expect(textBase.formattedText.spans.getItem(0).text).toBe('0'); + expect(textBase.formattedText.spans.getItem(1).text).toBe('1'); + expect(textBase.formattedText.spans.getItem(2).text).toBe('2'); + }); + }); + } +}); diff --git a/apps/nativescript-demo-ng/src/tests/textnode.spec.ts b/apps/nativescript-demo-ng/src/tests/textnode.spec.ts new file mode 100644 index 0000000..838fac0 --- /dev/null +++ b/apps/nativescript-demo-ng/src/tests/textnode.spec.ts @@ -0,0 +1,53 @@ +import { Component, ElementRef, NO_ERRORS_SCHEMA, ViewChild } from '@angular/core'; +import { TestBed } from '@angular/core/testing'; +import { TextNode } from '@nativescript/angular'; +import { TextBase } from '@nativescript/core'; + +@Component({ + template: ``, + schemas: [NO_ERRORS_SCHEMA], + standalone: true, +}) +class TextNodeComponent { + @ViewChild('textElement', { static: true }) textElement: ElementRef; + text = 'textnode'; +} + +@Component({ + template: ``, + schemas: [NO_ERRORS_SCHEMA], + standalone: true, +}) +class TextNodeSpansComponent { + @ViewChild('textElement', { static: true }) textElement: ElementRef; + text = 'textnode'; +} + +describe('TextNode', () => { + beforeEach(() => TestBed.configureTestingModule({ imports: [TextNodeComponent, TextNodeSpansComponent] })); + it('should create a text node', () => { + const textNode = new TextNode('foo'); + expect(textNode.text).toBe('foo'); + }); + it('should set text to Label', () => { + const fixture = TestBed.createComponent(TextNodeComponent); + fixture.detectChanges(); + const label = fixture.componentInstance.textElement.nativeElement; + expect(label.text).toBe('textnode'); + fixture.componentInstance.text = null; + fixture.detectChanges(); + expect(label.text).toBe(''); + }); + + it('should set text to Label with Spans', () => { + const fixture = TestBed.createComponent(TextNodeSpansComponent); + fixture.detectChanges(); + const label = fixture.componentInstance.textElement.nativeElement; + expect(label.text).toBe('textnode'); + fixture.componentInstance.text = null; + fixture.detectChanges(); + expect(label.text).toBe(''); + }); +}); diff --git a/packages/angular/src/lib/element-registry/common-views.ts b/packages/angular/src/lib/element-registry/common-views.ts index beed92e..3d8283c 100644 --- a/packages/angular/src/lib/element-registry/common-views.ts +++ b/packages/angular/src/lib/element-registry/common-views.ts @@ -1,5 +1,5 @@ import { AbsoluteLayout, ActivityIndicator, Button, ContentView, DatePicker, DockLayout, FlexboxLayout, FormattedString, Frame, GridLayout, HtmlView, Image, Label, ListPicker, ListView, Page, Placeholder, Progress, ProxyViewContainer, Repeater, RootLayout, ScrollView, SearchBar, SegmentedBar, SegmentedBarItem, Slider, Span, StackLayout, Switch, TabView, TextField, TextView, TimePicker, WebView, WrapLayout } from '@nativescript/core'; -import { frameMeta } from './metas'; +import { formattedStringMeta, frameMeta, textBaseMeta } from './metas'; import { registerElement } from './registry'; // Register default NativeScript components @@ -9,7 +9,7 @@ export function registerNativeScriptViewComponents() { (global).__ngRegisteredViews = true; registerElement('AbsoluteLayout', () => AbsoluteLayout); registerElement('ActivityIndicator', () => ActivityIndicator); - registerElement('Button', () => Button); + registerElement('Button', () => Button, textBaseMeta); registerElement('ContentView', () => ContentView); registerElement('DatePicker', () => DatePicker); registerElement('DockLayout', () => DockLayout); @@ -19,7 +19,7 @@ export function registerNativeScriptViewComponents() { registerElement('Image', () => Image); // Parse5 changes tags to . WTF! registerElement('img', () => Image); - registerElement('Label', () => Label); + registerElement('Label', () => Label, textBaseMeta); registerElement('ListPicker', () => ListPicker); registerElement('ListView', () => ListView); registerElement('Page', () => Page); @@ -37,12 +37,12 @@ export function registerNativeScriptViewComponents() { registerElement('FlexboxLayout', () => FlexboxLayout); registerElement('Switch', () => Switch); registerElement('TabView', () => TabView); - registerElement('TextField', () => TextField); - registerElement('TextView', () => TextView); + registerElement('TextField', () => TextField, textBaseMeta); + registerElement('TextView', () => TextView, textBaseMeta); registerElement('TimePicker', () => TimePicker); registerElement('WebView', () => WebView); registerElement('WrapLayout', () => WrapLayout); - registerElement('FormattedString', () => FormattedString); + registerElement('FormattedString', () => FormattedString, formattedStringMeta); registerElement('Span', () => Span); } } diff --git a/packages/angular/src/lib/element-registry/metas.ts b/packages/angular/src/lib/element-registry/metas.ts index 9cd27ea..6e70ea7 100644 --- a/packages/angular/src/lib/element-registry/metas.ts +++ b/packages/angular/src/lib/element-registry/metas.ts @@ -1,6 +1,6 @@ -import { Frame, Page } from '@nativescript/core'; -import { NgView, ViewClassMeta } from '../views/view-types'; +import { FormattedString, Frame, Page, Span, TextBase } from '@nativescript/core'; import { isInvisibleNode } from '../views/utils'; +import { NgView, ViewClassMeta } from '../views/view-types'; export const frameMeta: ViewClassMeta = { insertChild: (parent: Frame, child: NgView) => { @@ -14,3 +14,41 @@ export const frameMeta: ViewClassMeta = { } }, }; + +export const formattedStringMeta: ViewClassMeta = { + insertChild(parent: FormattedString, child: Span, next: Span) { + const index = parent.spans.indexOf(next); + if (index > -1) { + parent.spans.splice(index, 0, child); + } else { + parent.spans.push(child); + } + }, + removeChild(parent: FormattedString, child: Span) { + const index = parent.spans.indexOf(child); + if (index > -1) { + parent.spans.splice(index, 1); + } + }, +}; + +export const textBaseMeta: ViewClassMeta = { + insertChild(parent: TextBase, child, next) { + if (child instanceof FormattedString) { + parent.formattedText = child; + } else if (child instanceof Span) { + parent.formattedText ??= new FormattedString(); + formattedStringMeta.insertChild(parent.formattedText, child, next); + } + }, + removeChild(parent: TextBase, child: NgView) { + if (!parent.formattedText) return; + if (child instanceof FormattedString) { + if (parent.formattedText === child) { + parent.formattedText = null; + } + } else if (child instanceof Span) { + formattedStringMeta.removeChild(parent.formattedText, child); + } + }, +}; diff --git a/packages/angular/src/lib/view-util.ts b/packages/angular/src/lib/view-util.ts index 0b5e39f..a0d879c 100644 --- a/packages/angular/src/lib/view-util.ts +++ b/packages/angular/src/lib/view-util.ts @@ -1,7 +1,7 @@ -import { View, unsetValue, Placeholder, ContentView, LayoutBase, ProxyViewContainer } from '@nativescript/core'; +import { unsetValue, View } from '@nativescript/core'; import { getViewClass, getViewMeta, isKnownView } from './element-registry'; -import { CommentNode, NgView, TextNode, ViewExtensions, isDetachedElement, isInvisibleNode, isView, isContentView, isLayout } from './views'; import { NamespaceFilter } from './property-filter'; +import { CommentNode, isContentView, isDetachedElement, isInvisibleNode, isLayout, isView, NgView, TextNode } from './views'; import { NativeScriptDebug } from './trace'; import { NgLayoutBase } from './views/view-types'; @@ -61,6 +61,7 @@ function printSiblingsTree(view: NgView) { console.log(`${view} previousSiblings: ${previousSiblings} nextSiblings: ${nextSiblings}`); } +// eslint-disable-next-line @typescript-eslint/ban-types const propertyMaps: Map> = new Map>(); export class ViewUtil { @@ -371,10 +372,10 @@ export class ViewUtil { } private ensureNgViewExtensions(view: View): NgView { - if (view.hasOwnProperty('meta')) { + if (Object.hasOwnProperty.call(view, 'meta')) { return view as NgView; } else { - const name = view.cssType; + const name = view.cssType || view.typeName; const ngView = this.setNgViewExtensions(view, name); return ngView; @@ -501,8 +502,8 @@ export class ViewUtil { } if (!propertyMaps.has(type)) { - let propMap = new Map(); - for (let propName in instance) { + const propMap = new Map(); + for (const propName in instance) { // tslint:disable:forin propMap.set(propName.toLowerCase(), propName); } @@ -532,14 +533,14 @@ export class ViewUtil { } private setClasses(view: NgView, classesValue: string): void { - let classes = classesValue.split(whiteSpaceSplitter); + const classes = classesValue.split(whiteSpaceSplitter); this.cssClasses(view).clear(); classes.forEach((className) => this.cssClasses(view).set(className, true)); this.syncClasses(view); } private syncClasses(view: NgView): void { - let classValue = (Array).from(this.cssClasses(view).keys()).join(' '); + const classValue = (Array).from(this.cssClasses(view).keys()).join(' '); view.className = classValue; } diff --git a/packages/angular/src/lib/views/utils.ts b/packages/angular/src/lib/views/utils.ts index 46de283..4a5051e 100644 --- a/packages/angular/src/lib/views/utils.ts +++ b/packages/angular/src/lib/views/utils.ts @@ -1,4 +1,4 @@ -import { ContentView, LayoutBase, ProxyViewContainer, View } from '@nativescript/core'; +import { ContentView, LayoutBase, ProxyViewContainer, View, ViewBase } from '@nativescript/core'; import { InvisibleNode } from './invisible-nodes'; import type { NgContentView, NgLayoutBase, NgView } from './view-types';