首頁 web前端 js教程 Angular 18 中的 UI KIT 開發

Angular 18 中的 UI KIT 開發

Sep 13, 2024 pm 10:16 PM

Разработка UI KIT на Angular 18

讓我們建立應用程式的幾個元件。

在這種情況下,我們需要以下 UI:

  • 手風琴 - 手風琴;
  • autocomplete — 自動完成輸入;
  • 按鈕 - 按鈕;
  • 卡片 - 卡片;
  • 複選框 - 複選框;
  • 容器 - 內容居中;
  • datepicker - 日期選擇器;
  • 對話框 - 模態視​​窗;
  • 標題 - 促銷文字;
  • 圖示 — svg 圖示集;
  • 輸入 - 輸入;
  • label - 標籤;
  • 佈局-佈局;
  • 導航 - 選單;
  • 部分 — 內容中各部分的後台任務;
  • 標題 - 標題。

複雜元素將使用 Angular/cdk(對話框、手風琴)來簡化過程。

所以讓我們加入包裝:

yarn add -D @angular/cdk
登入後複製

還有一個 scss 的小魔法:

{
  "inlineStyleLanguage": "scss",
  "stylePreprocessorOptions": {
    "includePaths": ["node_modules", "./"]
  }
}
登入後複製

創建實用程式

向 ui 新增新的 utils 目錄:

mkdir src/app/ui/utils
mkdir src/app/ui/utils/lib
echo >src/app/ui/utils/index.ts
登入後複製

在 tsconfig.json 中我們將寫別名:

{
  "paths": {
    "@baf/ui/utils": ["src/app/ui/utils/index.ts"]
  }
}
登入後複製

讓我們在 types.ts 中定義幾種類型:

export type ButtonMode = 'primary' | 'secondary' | 'tertiary' | undefined;

export type Size = 'small' | 'medium' | 'large' | undefined;
export type Align = 'left' | 'center' | 'right' | undefined;
export type ExtraSize = 'xsmall' | 'small' | 'medium' | 'large' | 'xlarge' | undefined;
export type Width = 'max' | 'initial' | undefined;
登入後複製

它們都是可選的,因此它們包含未定義的。

組件的一些常見屬性:

  • 對齊 - 左、右居中;
  • 停用 - 停用狀態(與表單相關);
  • 尺寸、額外尺寸 - 文字尺寸小、中、大
  • mode - 按鈕類型;
  • width - 元素寬度。

對齊指令

對齊的範例實作:

import { Directive, inject, input } from '@angular/core';

import { ExtraClassService, toClass } from '@baf/core';

import type { Align } from './types';

@Directive({
  selector: '[bafAlign]',
  standalone: true,
  providers: [ExtraClassService],
})
export class AlignDirective {
  private readonly extraClassService = inject(ExtraClassService);

  readonly align = input<Align, Align>(undefined, {
    alias: 'bafAlign',
    transform: (value) => {
      this.extraClassService.update('align', toClass(value, 'align'));

      return value;
    },
  });
}
登入後複製

ExtraClassService - 新增對應類別的服務。

作為替代方案,您可以使用 @HostBinding('class.align-center') 或透過主機設定規則。

由於 Angular 不允許您在指令中包含樣式,因此我們新增一個需要為每個元件匯入的 mixin。

在src/stylesheets建立檔案align.scss:

@mixin make-align() {
  &.align-left {
    text-align: left;
  }

  &.align-center {
    text-align: center;
  }

  &.align-right {
    text-align: right;
  }
}
登入後複製

使用範例:

@use 'src/stylesheets/align' as align;
:host {
  @include align.make-align();
}
登入後複製

其餘指令類似。

容器

新增容器並輸入別名。

mkdir src/app/ui/container
mkdir src/app/ui/container/lib
echo >src/app/ui/container/index.ts
登入後複製

生成組件:

yarn ng g c container
登入後複製

將其移至 src/app/ui/container/lib 並編輯 ContainerComponent:

import { ChangeDetectionStrategy, Component } from '@angular/core';
import { RouterOutlet } from '@angular/router';

import { AlignDirective } from '@baf/ui/utils';

import { FluidDirective } from './fluid.directive';
import { MobileDirective } from './mobile.directive';

@Component({
  selector: 'baf-container',
  standalone: true,
  imports: [RouterOutlet],
  template: '<ng-content/>',
  styleUrl: './container.component.scss',
  changeDetection: ChangeDetectionStrategy.OnPush,
  host: {
    class: 'baf-container',
  },
  hostDirectives: [
    {
      directive: FluidDirective,
      inputs: ['bafFluid'],
    },
    {
      directive: MobileDirective,
      inputs: ['bafMobile'],
    },
    {
      directive: AlignDirective,
      inputs: ['bafAlign'],
    },
  ],
})
export class ContainerComponent {}
登入後複製

新增樣式:

@use 'src/stylesheets/align' as align;
@use 'src/stylesheets/device' as device;

:host {
  display: flex;
  flex-direction: column;
  margin-left: auto;
  margin-right: auto;
  width: 100%;

  &.fluid {
    max-width: 100%;
  }

  &:not(.mobile-no-gutter) {
    padding-left: 1rem;
    padding-right: 1rem;
  }

  @include align.make-align();

  @include device.media-tablet-portrait() {
    &:not(.fluid) {
      max-width: 788px;
    }
  }

  @include device.media-tablet-landscape() {
    &:not(.fluid) {
      max-width: 928px;
    }
  }

  @include device.media-web-portrait() {
    &:not(.fluid) {
      max-width: 808px;
    }
  }

  @include device.media-web-landscape() {
    &:not(.fluid) {
      max-width: 1200px;
    }
  }
}
登入後複製

取自材質的寬度混合:

@mixin media-handset() {
  @media (max-width: 599.98px) and (orientation: portrait), (max-width: 959.98px) and (orientation: landscape) {
    @content;
  }
}
@mixin media-handset-up() {
  @media (min-width: 0) and (orientation: portrait), (min-width: 0) and (orientation: landscape) {
    @content;
  }
}

@mixin media-handset-portrait() {
  @media (max-width: 599.98px) and (orientation: portrait) {
    @content;
  }
}

@mixin media-handset-landscape() {
  @media (max-width: 959.98px) and (orientation: landscape) {
    @content;
  }
}

@mixin media-tablet() {
  @media (min-width: 600px) and (max-width: 839.98px) and (orientation: portrait),
    (min-width: 960px) and (max-width: 1279.98px) and (orientation: landscape) {
    @content;
  }
}

@mixin media-tablet-up() {
  @media (min-width: 600px) and (orientation: portrait), (min-width: 960px) and (orientation: landscape) {
    @content;
  }
}

@mixin media-tablet-landscape() {
  @media (min-width: 960px) and (max-width: 1279.98px) and (orientation: landscape) {
    @content;
  }
}

@mixin media-tablet-portrait() {
  @media (min-width: 600px) and (max-width: 839.98px) and (orientation: portrait) {
    @content;
  }
}

@mixin media-web() {
  @media (min-width: 840px) and (orientation: portrait), (min-width: 1280px) and (orientation: landscape) {
    @content;
  }
}

@mixin media-web-up() {
  @media (min-width: 840px) and (orientation: portrait), (min-width: 1280px) and (orientation: landscape) {
    @content;
  }
}

@mixin media-web-portrait() {
  @media (min-width: 840px) and (orientation: portrait) {
    @content;
  }
}

@mixin media-web-landscape() {
  @media (min-width: 1280px) and (orientation: landscape) {
    @content;
  }
}
登入後複製

我們也會建立兩個指令 FluidDirective 和 MobileDirective:

import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { Directive, inject, input } from '@angular/core';

import type { CoerceBoolean } from '@baf/core';
import { ExtraClassService } from '@baf/core';

@Directive({
  selector: 'baf-container[bafFluid]',
  standalone: true,
  providers: [ExtraClassService],
})
export class FluidDirective {
  private readonly extraClassService = inject(ExtraClassService);

  readonly fluid = input<CoerceBoolean, CoerceBoolean>(undefined, {
    alias: 'bafFluid',
    transform: (value) => {
      this.extraClassService.patch('fluid', coerceBooleanProperty(value));

      return value;
    },
  });
}
登入後複製
import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { Directive, inject, input } from '@angular/core';

import type { CoerceBoolean } from '@baf/core';
import { ExtraClassService } from '@baf/core';

@Directive({
  selector: 'baf-container[bafMobile]',
  standalone: true,
  providers: [ExtraClassService],
})
export class MobileDirective {
  private readonly extraClassService = inject(ExtraClassService);

  readonly mobile = input<CoerceBoolean, CoerceBoolean>(undefined, {
    alias: 'bafMobile',
    transform: (value) => {
      this.extraClassService.patch('mobile-no-gutter', coerceBooleanProperty(value));

      return value;
    },
  });
}
登入後複製

標題、標籤、標題、部分和卡片

新增標題並輸入別名:

mkdir src/app/ui/title
mkdir src/app/ui/title/lib
echo >src/app/ui/title/index.ts
登入後複製

生成組件:

yarn ng g c title
登入後複製

將其移至 src/app/ui/title/lib 並編輯 TitleComponent:

import { ChangeDetectionStrategy, Component } from '@angular/core';

import { AlignDirective, SizeDirective } from '@baf/ui/utils';

@Component({
  selector: 'baf-title,[baf-title],[bafTitle]',
  standalone: true,
  template: '<ng-content/>',
  styleUrl: './title.component.scss',
  changeDetection: ChangeDetectionStrategy.OnPush,
  host: {
    class: 'baf-title',
  },
  hostDirectives: [
    {
      directive: SizeDirective,
      inputs: ['bafSize'],
    },
    {
      directive: AlignDirective,
      inputs: ['bafAlign'],
    },
  ],
})
export class TitleComponent {}
登入後複製

一些款式:

@use 'src/stylesheets/align' as align;
@use 'src/stylesheets/size' as size;
@use 'src/stylesheets/typography' as typography;

:host {
  @include size.make-size() using ($size) {
    @if $size == small {
      @include typography.title-small();
    } @else if $size == medium {
      @include typography.title-medium();
    } @else if $size == large {
      @include typography.title-large();
    }
  }
  @include align.make-align();
}
登入後複製

剩下的我們以同樣的方式進行。

按鈕

讓我們繼續創建更複雜的小部件。

新增按鈕並輸入別名:

mkdir src/app/ui/buttons
mkdir src/app/ui/buttons/lib
echo >src/app/ui/buttons/index.ts
登入後複製

由於需要多種類型的按鈕,我們將設定基本類型 -ButtonBase 和 AnchorBase:

import type { FocusOrigin } from '@angular/cdk/a11y';
import { FocusMonitor } from '@angular/cdk/a11y';
import { coerceBooleanProperty } from '@angular/cdk/coercion';
import type { AfterViewInit, OnDestroy, OnInit } from '@angular/core';
import { Directive, ElementRef, inject, NgZone } from '@angular/core';

@Directive()
export class ButtonBase implements AfterViewInit, OnDestroy {
  protected readonly elementRef = inject(ElementRef);

  private isDisabled = false;

  private readonly focusMonitor = inject(FocusMonitor);

  get disabled(): boolean {
    return this.isDisabled;
  }

  set disabled(value: string | boolean | null | undefined) {
    const disabled = coerceBooleanProperty(value);
    if (disabled !== this.isDisabled) {
      this.isDisabled = disabled;
    }
  }

  ngAfterViewInit() {
    this.focusMonitor.monitor(this.elementRef, true);
  }

  ngOnDestroy() {
    this.focusMonitor.stopMonitoring(this.elementRef);
  }

  focus(origin: FocusOrigin = 'program', options?: FocusOptions): void {
    if (origin) {
      this.focusMonitor.focusVia(this.elementRef.nativeElement, origin, options);
    } else {
      this.elementRef.nativeElement.focus(options);
    }
  }
}

@Directive()
export class AnchorBase extends ButtonBase implements OnInit, OnDestroy {
  private readonly ngZone = inject(NgZone);

  protected readonly haltDisabledEvents = (event: Event) => {
    if (this.disabled) {
      event.preventDefault();
      event.stopImmediatePropagation();
    }
  };

  ngOnInit(): void {
    this.ngZone.runOutsideAngular(() => {
      this.elementRef.nativeElement.addEventListener('click', this.haltDisabledEvents);
    });
  }

  override ngOnDestroy(): void {
    super.ngOnDestroy();
    this.elementRef.nativeElement.removeEventListener('click', this.haltDisabledEvents);
  }
}
登入後複製

實作取自 Angular Material 2。

我還沒看過 Angular 中的 Material 3,我認為不值得。組件的複雜度略大於無窮大。

從範例中可以看到,定義了停用狀態以及焦點觀察者。

讓我們建立一個簡單的按鈕。在我們的例子中,這是標準包裝的包裝。

模板 - .

import { ChangeDetectionStrategy, Component } from '@angular/core';

import { DisabledDirective, ExtraSizeDirective, ModeDirective, WidthDirective } from '@baf/ui/utils';

import { AnchorBase, ButtonBase } from '../base/button-base';

@Component({
  // eslint-disable-next-line @angular-eslint/component-selector
  selector: 'button[baf-button]',
  standalone: true,
  template: '<ng-content />',
  styleUrl: './button.component.scss',
  changeDetection: ChangeDetectionStrategy.OnPush,
  host: {
    class: 'baf-button',
  },
  hostDirectives: [
    {
      directive: ModeDirective,
      inputs: ['bafMode'],
    },
    {
      directive: ExtraSizeDirective,
      inputs: ['bafSize'],
    },
    {
      directive: DisabledDirective,
      inputs: ['disabled'],
    },
    {
      directive: WidthDirective,
      inputs: ['bafWidth'],
    },
  ],
})
export class ButtonComponent extends ButtonBase {}

@Component({
  // eslint-disable-next-line @angular-eslint/component-selector
  selector: 'a[baf-button]',
  standalone: true,
  template: '<ng-content />',
  styleUrls: ['./button.component.scss'],
  changeDetection: ChangeDetectionStrategy.OnPush,
  host: {
    class: 'baf-button',
  },
  hostDirectives: [
    {
      directive: ModeDirective,
      inputs: ['bafMode'],
    },
    {
      directive: ExtraSizeDirective,
      inputs: ['bafSize'],
    },
    {
      directive: DisabledDirective,
      inputs: ['disabled'],
    },
    {
      directive: WidthDirective,
      inputs: ['bafWidth'],
    },
  ],
})
export class AnchorComponent extends AnchorBase {}
登入後複製

В компоненте определены общие директивы, в которые и вынесена вся логика.

Немного SCSS:

@use 'src/stylesheets/button' as button;
@use 'src/stylesheets/width' as width;

:host {
  display: inline-flex;
  flex-direction: row;
  align-items: center;
  justify-content: center;
  padding: 0.5rem 1.5rem;

  border: none;
  box-shadow: none;
  border-radius: 3px;
  cursor: pointer;
  text-decoration: none;

  &.mode-primary {
    @include button.mode(--md-sys-color-primary-container, --md-sys-color-on-primary, --md-sys-color-primary);
  }

  &.mode-secondary {
    @include button.mode(--md-sys-color-secondary-container, --md-sys-color-on-secondary, --md-sys-color-secondary);
  }

  &.mode-tertiary {
    @include button.mode(--md-sys-color-tertiary-container, --md-sys-color-on-tertiary, --md-sys-color-tertiary);
  }

  @include button.disabled();
  @include button.sizes();
  @include width.make-width();
}
登入後複製

Теперь создадим icon-button.

Макет:

<span class="icon-content">
  <ng-content />
</span>
<span class="state-layer"></span>
登入後複製
import { ChangeDetectionStrategy, Component } from '@angular/core';

import { DisabledDirective, ExtraSizeDirective, ModeDirective } from '@baf/ui/utils';

import { AnchorBase, ButtonBase } from '../base/button-base';

@Component({
  // eslint-disable-next-line @angular-eslint/component-selector
  selector: 'button[baf-icon-button]',
  standalone: true,
  templateUrl: './icon-button.component.html',
  styleUrl: './icon-button.component.scss',
  changeDetection: ChangeDetectionStrategy.OnPush,
  host: {
    class: 'baf-icon-button',
  },
  hostDirectives: [
    {
      directive: ModeDirective,
      inputs: ['bafMode'],
    },
    {
      directive: ExtraSizeDirective,
      inputs: ['bafSize'],
    },
    {
      directive: DisabledDirective,
      inputs: ['disabled'],
    },
  ],
})
export class IconButtonComponent extends ButtonBase {}

@Component({
  // eslint-disable-next-line @angular-eslint/component-selector
  selector: 'a[baf-icon-button]',
  standalone: true,
  templateUrl: './icon-button.component.html',
  styleUrl: './icon-button.component.scss',
  changeDetection: ChangeDetectionStrategy.OnPush,
  host: {
    class: 'baf-icon-button',
  },
  hostDirectives: [
    {
      directive: ModeDirective,
      inputs: ['bafMode'],
    },
    {
      directive: ExtraSizeDirective,
      inputs: ['bafSize'],
    },
    {
      directive: DisabledDirective,
      inputs: ['disabled'],
    },
  ],
})
export class IconAnchorComponent extends AnchorBase {}
登入後複製

Стилизуем кнопки:

:host {
  display: inline-flex;
  flex-direction: row;
  flex-wrap: nowrap;
  align-items: center;
  justify-content: center;

  height: 48px;
  width: 48px;
  padding: 4px;
  border: none;
  z-index: 0;
  gap: 8px;
  white-space: nowrap;
  user-select: none;
  background-color: transparent;
  text-decoration: none;

  cursor: pointer;

  border-radius: var(--md-sys-shape-corner-full);

  position: relative;
}

.state-layer {
  position: absolute;
  width: 100%;
  height: 100%;
  left: 0;
  top: 0;
  right: 0;
  bottom: 0;
  display: block;
  z-index: 1;
  opacity: 0;
  border-radius: inherit;
}

.icon-content {
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--md-sys-color-on-surface-variant);
  fill: var(--md-sys-color-on-surface-variant);
  line-height: 1;

  :host:hover & {
    color: var(--md-sys-color-on-surface);
    fill: var(--md-sys-color-on-surface);
  }
}
登入後複製

Icons

Добавим компонент для иконок.

mkdir src/app/ui/icons
mkdir src/app/ui/icons/lib
echo >src/app/ui/icons/index.ts
登入後複製

Запускаем команду:

yarn ng g c icon
登入後複製

Переносим его в src/app/ui/title/lib и отредактируем IconComponent:

import { ChangeDetectionStrategy, Component } from '@angular/core';

@Component({
  // eslint-disable-next-line @angular-eslint/component-selector
  selector: 'svg[baf-icon]',
  standalone: true,
  imports: [],
  templateUrl: './icon.component.html',
  styleUrl: './icon.component.scss',
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class IconComponent {}
登入後複製

Стили:

src/app/ui/icons/lib/icon/icon.component.scss
登入後複製

Пример использования на иконке домой:

<svg baf-icon xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px">
  <path d="M240-200h120v-240h240v240h120v-360L480-740 240-560v360Zm-80 80v-480l320-240 320 240v480H520v-240h-80v240H160Zm320-350Z" />
</svg>
登入後複製
import { ChangeDetectionStrategy, Component } from '@angular/core';

import { IconComponent } from '../icon/icon.component';

@Component({
  selector: 'baf-icon-home',
  standalone: true,
  imports: [IconComponent],
  templateUrl: './home.component.html',
  styleUrl: './home.component.scss',
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class HomeComponent {}
登入後複製

Так же созданы все остальные иконки:

  • arrow-down - стрелка вниз;
  • arrow-up - стрелка вверх;
  • chevron-left - стрелка влево;
  • chevron-right - стрелка вправо;
  • home - дом;
  • logo - лого;
  • star - звезда;
  • sync-alt - рефреш.

Accordion

Реализуем аккордеон:

mkdir src/app/ui/accordion
mkdir src/app/ui/accordion/lib
echo >src/app/ui/accordion/index.ts
登入後複製

Запускаем команду:

yarn ng g c accordion
登入後複製

Добавим интерфейс:

export interface AccordionItem {
  readonly title: string;
  readonly description: string;
}
登入後複製

В компоненте будем выводить список элементов:

import { CdkAccordionModule } from '@angular/cdk/accordion';
import { ChangeDetectionStrategy, Component, input } from '@angular/core';

import { ArrowDownComponent, ArrowUpComponent } from '@baf/ui/icons';

export interface AccordionItem {
  readonly title: string;
  readonly description: string;
}

@Component({
  selector: 'baf-accordion',
  standalone: true,
  imports: [CdkAccordionModule, ArrowDownComponent, ArrowUpComponent],
  templateUrl: './accordion.component.html',
  styleUrl: './accordion.component.scss',
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AccordionComponent {
  readonly items = input.required<AccordionItem[]>();
}
登入後複製

Шаблон:

<cdk-accordion>
  @for (item of items(); track item.title; let index = $index) {
    <cdk-accordion-item
      class="accordion"
      #accordionItem="cdkAccordionItem"
      role="button"
      tabindex="0"
      [attr.id]="'accordion-header-' + index"
      [attr.aria-expanded]="accordionItem.expanded"
      [attr.aria-controls]="'accordion-body-' + index"
    >
      <div class="accordion-header" (click)="accordionItem.toggle()">
        @if (accordionItem.expanded) {
          <baf-arrow-down />
        } @else {
          <baf-arrow-up />
        }
        <span> {{ item.title }} </span>
      </div>
      <div
        class="accordion-body"
        role="region"
        [style.display]="accordionItem.expanded ? '' : 'none'"
        [attr.id]="'accordion-body-' + index"
        [attr.aria-labelledby]="'accordion-header-' + index"
      >
        {{ item.description }}
      </div>
    </cdk-accordion-item>
  }
</cdk-accordion>
登入後複製

Стили:

.accordion {
  display: block;

  &:not(:last-child) {
    border-bottom: 1px solid var(--md-sys-color-surface-variant);
    padding-bottom: 1rem;
    margin-bottom: 1rem;
  }
}

.accordion-header {
  display: flex;
  flex-direction: row;
  align-items: center;
  gap: 0.5rem;
  cursor: pointer;
  line-height: 1;
  user-select: none;
}

.accordion-body {
  padding: 1rem 1rem 0 2rem;
}
登入後複製

Checkbox

Создадим чекбокс.

Отмечу, что оформление я взял из проекта мериалайз

mkdir src/app/ui/checkbox
mkdir src/app/ui/checkbox/lib
echo >src/app/ui/checkbox/index.ts
登入後複製

Запускаем команду:

yarn ng g c checkbox
登入後複製

Разметка:

<label>
  <input type="checkbox" [name]="options().name ?? ''" [formControl]="control()" />
  <span><ng-content /></span>
</label>
登入後複製

Украду немного стилей из Material CSS:

[type='checkbox']:not(:checked),
[type='checkbox']:checked {
  position: absolute;
  opacity: 0;
  pointer-events: none;
}

[type='checkbox']:checked {
  + span:before {
    top: -4px;
    left: -5px;
    width: 12px;
    height: 22px;
    border-top: 2px solid transparent;
    border-left: 2px solid transparent;
    border-right: 2px solid var(--md-sys-color-primary);
    border-bottom: 2px solid var(--md-sys-color-primary);
    transform: rotate(40deg);
    backface-visibility: hidden;
    transform-origin: 100% 100%;
  }

  &:disabled + span:before {
    border-right: 2px solid var(--md-sys-color-shadow);
    border-bottom: 2px solid var(--md-sys-color-shadow);
  }
}

[type='checkbox'] {
  + span {
    position: relative;
    padding-left: 35px;
    cursor: pointer;
    display: inline-block;
    height: 25px;
    line-height: 25px;
    font-size: 1rem;
    user-select: none;
  }

  &:not(:checked):disabled + span:before {
    border: none;
    background-color: var(--md-sys-color-shadow);
  }

  // General
  + span:after {
    border-radius: 2px;
  }

  + span:before,
  + span:after {
    content: '';
    left: 0;
    position: absolute;
    /* .1s delay is for check animation */
    transition:
      border 0.25s,
      background-color 0.25s,
      width 0.2s 0.1s,
      height 0.2s 0.1s,
      top 0.2s 0.1s,
      left 0.2s 0.1s;
    z-index: 1;
  }

  // Unchecked style
  &:not(:checked) + span:before {
    width: 0;
    height: 0;
    border: 3px solid transparent;
    left: 6px;
    top: 10px;
    transform: rotateZ(37deg);
    transform-origin: 100% 100%;
  }

  &:not(:checked) + span:after {
    height: 20px;
    width: 20px;
    background-color: transparent;
    border: 2px solid var(--md-sys-color-outline);
    top: 0;
    z-index: 0;
  }

  // Checked style
  &:checked {
    + span:before {
      top: 0;
      left: 1px;
      width: 8px;
      height: 13px;
      border-top: 2px solid transparent;
      border-left: 2px solid transparent;
      border-right: 2px solid var(--md-sys-color-on-primary);
      border-bottom: 2px solid var(--md-sys-color-on-primary);
      transform: rotateZ(37deg);
      transform-origin: 100% 100%;
    }

    + span:after {
      top: 0;
      width: 20px;
      height: 20px;
      border: 2px solid var(--md-sys-color-primary);
      background-color: var(--md-sys-color-primary);
      z-index: 0;
    }
  }

  // Disabled style
  &:disabled:not(:checked) + span:before {
    background-color: transparent;
    border: 2px solid transparent;
  }

  &:disabled:not(:checked) + span:after {
    border-color: transparent;
    background-color: var(--md-sys-color-outline);
  }

  &:disabled:checked + span:before {
    background-color: transparent;
  }

  &:disabled:checked + span:after {
    background-color: var(--md-sys-color-outline);
    border-color: var(--md-sys-color-outline);
  }
}
登入後複製

Сам компонент:

import { ChangeDetectionStrategy, Component, input } from '@angular/core';
import type { FormControl } from '@angular/forms';
import { ReactiveFormsModule } from '@angular/forms';

export interface CheckboxOptions {
  readonly [key: string]: unknown;

  readonly name?: string;
}

@Component({
  selector: 'baf-checkbox',
  standalone: true,
  imports: [ReactiveFormsModule],
  templateUrl: './checkbox.component.html',
  styleUrl: './checkbox.component.scss',
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class CheckboxComponent {
  readonly control = input.required<FormControl<boolean>>();
  readonly options = input<CheckboxOptions>({});
}
登入後複製

Input

Реализуем инпут:

mkdir src/app/ui/input
mkdir src/app/ui/input/lib
echo >src/app/ui/input/index.ts
登入後複製

Выполним инструкцию:

yarn ng g c input
登入後複製

InputComponent будет оберткой над input.

import { ChangeDetectionStrategy, Component, ElementRef, inject } from '@angular/core';
import { NgControl } from '@angular/forms';

@Component({
  // eslint-disable-next-line @angular-eslint/component-selector
  selector: 'input[baf-input]',
  standalone: true,
  imports: [],
  template: '<ng-content/>',
  styleUrl: './input.component.scss',
  changeDetection: ChangeDetectionStrategy.OnPush,
  host: {
    class: 'baf-input',
  },
})
export class InputComponent {
  readonly elementRef: ElementRef<HTMLInputElement> = inject(ElementRef);
  readonly ngControl = inject(NgControl);
}
登入後複製

Добавим SCSS:

:host {
  display: block;
  background-color: transparent;
  height: 100%;
  width: 100%;
  padding: 0;
  border: none;
  outline: none;

  &:hover,
  &:focus,
  &:active {
    outline: none;
  }

  &::placeholder {
    color: var(--md-sys-color-on-surface-variant);
  }

  :host-context(.is-invalid) {
    color: var(--md-sys-color-error);
  }
}
登入後複製

Перенесем концепты из mat-form-field:

yarn ng g c input-control
登入後複製

Разметка и стили:

<div class="input-container">
  <ng-content select="[baf-input-prefix]" />
  <div class="input-box">
    <ng-content select="label[baf-label],baf-label" />
    <div class="input">
      <ng-content select="input[baf-input],baf-input" />
    </div>
  </div>
  <ng-content select="[baf-input-suffix]" />
</div>
<ng-content />
登入後複製
:host {
  display: flex;
  flex-direction: column;
  position: relative;
  width: 100%;

  &.is-disabled {
    cursor: not-allowed;
    pointer-events: none;
    color: rgba(var(--md-sys-color-on-surface-rgba), 0.38);

    .input {
      color: rgba(var(--md-sys-color-on-surface-rgba), 0.38);
    }
  }

  &.is-pressed,
  &.is-value {
    .input {
      opacity: 1;
    }
  }
}

.input-box {
  position: relative;
  margin: 0 16px;
  justify-content: center;
  height: 100%;
  flex-grow: 1;
}

.input-container {
  display: flex;
  flex-direction: row;
  flex-wrap: nowrap;
  align-items: center;
  background-color: var(--md-sys-color-surface-variant);
  color: var(--md-sys-color-on-surface-variant);
  border-radius: var(--md-sys-shape-corner-extra-small-top);
  height: 3rem;
}

.input {
  opacity: 0;
  height: 100%;
  width: 100%;
  position: relative;
  z-index: 2;
  transition: opacity 0.1s;
  padding: 12px 0 0 0;
}
登入後複製

Возможно можно и разбить на несколько дочерних компонентов, но и так получается достаточно сложно.

Используемые вспомогательные директивы для префиксов и прочего.

src/app/ui/input/lib/input-display.directive.ts:

import { Directive, ElementRef, forwardRef, inject, input } from '@angular/core';
import type { ControlValueAccessor } from '@angular/forms';
import { NG_VALUE_ACCESSOR } from '@angular/forms';

import type { ChangeFn, DisplayFn, TouchedFn } from '@baf/core';

@Directive({
  selector: 'input[formControlName][bafInputDisplay],input[formControl][bafInputDisplay]',
  standalone: true,
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      useExisting: forwardRef(() => InputDisplayDirective),
      multi: true,
    },
  ],
  host: {
    '(blur)': 'onTouched()',
    '(input)': 'onInput($event)',
  },
})
export class InputDisplayDirective implements ControlValueAccessor {
  private readonly elementRef = inject(ElementRef<HTMLInputElement>);
  readonly display = input.required<DisplayFn>({ alias: 'bafInputDisplay' });

  onChange!: ChangeFn;
  onTouched!: TouchedFn;

  registerOnChange(fn: ChangeFn): void {
    this.onChange = fn;
  }

  registerOnTouched(fn: TouchedFn): void {
    this.onTouched = fn;
  }

  writeValue(value: unknown): void {
    this.elementRef.nativeElement.value = this.display()(value);
  }

  onInput(event: Event): void {
    const { value } = event.target as HTMLInputElement;
    this.elementRef.nativeElement.value = this.display()(value);
    this.onChange(value);
  }
}
登入後複製

src/app/ui/input/lib/input-mask.directive.ts:

import type { OnInit } from '@angular/core';
import { Directive, ElementRef, forwardRef, inject, InjectionToken, input } from '@angular/core';
import type { ControlValueAccessor } from '@angular/forms';
import { NG_VALUE_ACCESSOR } from '@angular/forms';

import type { ChangeFn, MaskFn, TouchedFn } from '@baf/core';

export const INPUT_MASK_VALUES = new InjectionToken<Record<string, RegExp>>('INPUT_MASK_VALUES');

const DEFAULT_INPUT_MASK_VALUES: Record<string, RegExp> = { 0: /[0-9]/, a: /[a-z]/, A: /[A-Z]/, B: /[a-zA-Z]/ };

export const DEFAULT_MASK_FN: MaskFn = (value) => value;

@Directive({
  selector: 'input[formControlName][bafInputMask],input[formControl][bafInputMask]',
  standalone: true,
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      useExisting: forwardRef(() => InputMaskDirective),
      multi: true,
    },
  ],
  host: {
    '(blur)': 'onTouched()',
    '(input)': 'onInput($event)',
  },
})
export class InputMaskDirective implements ControlValueAccessor, OnInit {
  private readonly maskValues = inject(INPUT_MASK_VALUES, { optional: true }) ?? DEFAULT_INPUT_MASK_VALUES;
  private readonly elementRef = inject(ElementRef<HTMLInputElement>);

  private lastValue?: string;

  private readonly maskFormats = `(${Object.keys(this.maskValues)
    .map((key) => {
      const regexStr = this.maskValues[key].toString();

      return regexStr.substring(1, regexStr.length - 1);
    })
    .join('|')})`;

  readonly mask = input.required<string>({ alias: 'bafInputMask' });
  readonly maskFrom = input<MaskFn>(DEFAULT_MASK_FN, { alias: 'bafInputMaskFrom' });
  readonly maskTo = input<MaskFn>(DEFAULT_MASK_FN, { alias: 'bafInputMaskTo' });

  onChange!: ChangeFn;
  onTouched!: TouchedFn;

  registerOnChange(fn: ChangeFn): void {
    this.onChange = fn;
  }

  registerOnTouched(fn: TouchedFn): void {
    this.onTouched = fn;
  }

  writeValue(value: string | undefined | null): void {
    this.elementRef.nativeElement.value = this.getMaskedValue(this.maskTo()(value));
  }

  onInput(event: Event): void {
    const { value } = event.target as HTMLInputElement;
    const masked = this.getMaskedValue(value);
    this.elementRef.nativeElement.value = masked;
    this.onChange(this.maskFrom()(masked));
  }

  ngOnInit(): void {
    if (!this.mask()) {
      console.warn(`Property mask should not be empty for input:`, this.elementRef.nativeElement);
    }
  }

  getMaskedValue(value: string | undefined | null): string | undefined | null {
    if (!this.mask() || !value || value === this.lastValue) {
      return value;
    }

    const masked = this.valueToFormat(value, this.mask(), this.lastValue ? this.lastValue.length > value.length : false, this.lastValue);
    this.lastValue = masked;

    return masked;
  }

  /**
   * @see https://gist.github.com/rami-alloush/3ee792fd0647b73de5f863a2719c78c6
   */
  private valueToFormat(value: string, format: string, goingBack?: boolean, prevValue?: string): string {
    let maskedValue = '';
    const unmaskedValue = value.replace(' ', '').match(new RegExp(this.maskFormats, 'g'))?.join('') ?? '';

    const formats = new RegExp(this.maskFormats);
    const isLastCharFormatter = !formats.test(value[value.length - 1]);
    const isPrevLastCharFormatter = prevValue && !formats.test(prevValue[prevValue.length - 1]);

    let formatOffset = 0;
    for (let index = 0, max = Math.min(unmaskedValue.length, format.length); index < max; ++index) {
      const valueChar = unmaskedValue[index];
      let formatChar = format[formatOffset + index];
      let formatRegex = this.maskValues[formatChar];

      if (formatChar && !formatRegex) {
        maskedValue += formatChar;
        formatChar = format[++formatOffset + index];
        formatRegex = this.maskValues[formatChar];
      }

      if (valueChar && formatRegex) {
        if (formatRegex && formatRegex.test(valueChar)) {
          maskedValue += valueChar;
        } else {
          break;
        }
      }

      const nextFormatChar = format[formatOffset + index + 1];
      const nextFormatRegex = this.maskValues[nextFormatChar];
      const isLastIteration = index === max - 1;

      if (isLastIteration && nextFormatChar && !nextFormatRegex) {
        if (!isLastCharFormatter && goingBack) {
          if (prevValue && !isPrevLastCharFormatter) {
            continue;
          }
          maskedValue = maskedValue.substring(0, formatOffset + index);
        } else {
          maskedValue += nextFormatChar;
        }
      }
    }

    return maskedValue;
  }
}
登入後複製

src/app/ui/input/lib/input-prefix.directive.ts:

import { Directive } from '@angular/core';

@Directive({
  selector: '[bafInputPrefix]',
  standalone: true,
  host: {
    class: 'input-prefix',
    '[style.margin-left]': '"12px"',
  },
})
export class InputPrefixDirective {}
登入後複製

src/app/ui/input/lib/input-suffix.directive.ts:

import { Directive } from '@angular/core';

@Directive({
  selector: '[bafInputSuffix]',
  standalone: true,
  host: {
    class: 'baf-input-suffix',
    '[style.margin-right]': '"12px"',
  },
})
export class InputSuffixDirective {}
登入後複製

Логика работы достаточно проста:

import type { AfterViewInit, OnDestroy } from '@angular/core';
import { ChangeDetectionStrategy, Component, contentChild, DestroyRef, ElementRef, inject, Renderer2 } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import type { FormControlStatus } from '@angular/forms';
import { TouchedChangeEvent } from '@angular/forms';
import { filter, startWith, tap } from 'rxjs';

import { LabelComponent } from '@baf/ui/label';

import { InputComponent } from './input.component';

@Component({
  selector: 'baf-input-control',
  templateUrl: './input-control.component.html',
  styleUrls: ['./input-control.component.scss'],
  changeDetection: ChangeDetectionStrategy.OnPush,
  standalone: true,
  host: {
    class: 'baf-input-control',
  },
})
export class InputControlComponent implements AfterViewInit, OnDestroy {
  readonly destroyRef = inject(DestroyRef);
  readonly elementRef: ElementRef<HTMLInputElement> = inject(ElementRef);
  readonly renderer = inject(Renderer2);

  readonly label = contentChild<LabelComponent>(LabelComponent);
  readonly input = contentChild.required<InputComponent>(InputComponent);

  private isDisabled = false;

  ngAfterViewInit(): void {
    const input = this.input();
    if (!input) {
      console.warn('Input[baf-input] not found. Add child <input baf-input /> in <baf-input-control></baf-input-control>');
      return;
    }

    input.elementRef.nativeElement.addEventListener('click', this.onFocusin);
    input.elementRef.nativeElement.addEventListener('focusout', this.onFocusout);
    input.elementRef.nativeElement.addEventListener('input', this.onInput);
    input.elementRef.nativeElement.addEventListener('change', this.onInput);
    this.onInput({ target: input.elementRef.nativeElement });

    input.ngControl.control?.events
      .pipe(
        filter((event) => event instanceof TouchedChangeEvent),
        tap(() => this.check()),
        takeUntilDestroyed(this.destroyRef),
      )
      .subscribe();

    input.ngControl.valueChanges
      ?.pipe(
        tap(() => {
          if (!input.ngControl.value && this.elementRef.nativeElement.classList.contains('is-value')) {
            this.renderer.removeClass(this.elementRef.nativeElement, 'is-value');
          }
          this.onInput({ target: input.elementRef.nativeElement });
        }),
        takeUntilDestroyed(this.destroyRef),
      )
      .subscribe();

    input.ngControl.statusChanges
      ?.pipe(
        startWith(input.ngControl.status),
        tap((status: FormControlStatus) => {
          this.isDisabled = status === 'DISABLED';
          this.disable();
        }),
        takeUntilDestroyed(this.destroyRef),
      )
      .subscribe();
  }

  ngOnDestroy(): void {
    const input = this.input();
    if (!input) {
      return;
    }

    input.elementRef.nativeElement.removeEventListener('click', this.onFocusin);
    input.elementRef.nativeElement.removeEventListener('focusout', this.onFocusout);
    input.elementRef.nativeElement.removeEventListener('input', this.onInput);
    input.elementRef.nativeElement.removeEventListener('change', this.onInput);
  }

  private onFocusin = () => {
    if (!this.isDisabled) {
      this.renderer.addClass(this.elementRef.nativeElement, 'is-pressed');
    }
  };

  private onFocusout = () => {
    if (!this.isDisabled) {
      this.renderer.removeClass(this.elementRef.nativeElement, 'is-pressed');
    }
    this.check();
  };

  private onInput = (event: Event | { target: HTMLInputElement }) => {
    if (!this.isDisabled) {
      const target = event.target as HTMLInputElement;

      if (target.value?.length > 0) {
        this.renderer.addClass(this.elementRef.nativeElement, 'is-value');
      } else {
        this.renderer.removeClass(this.elementRef.nativeElement, 'is-value');
      }

      this.check();
    }
  };

  private disable(): void {
    if (this.isDisabled) {
      this.renderer.addClass(this.elementRef.nativeElement, 'is-disabled');
    } else {
      this.renderer.removeClass(this.elementRef.nativeElement, 'is-disabled');
    }
  }

  private check(): void {
    if (this.input().ngControl.touched) {
      if (this.input().ngControl.errors) {
        this.renderer.addClass(this.elementRef.nativeElement, 'is-invalid');
      } else {
        this.renderer.removeClass(this.elementRef.nativeElement, 'is-invalid');
      }
    }
  }
}
登入後複製

Так как input является потомком, ищем его после рендера и добавляем обработчики:

input.elementRef.nativeElement.addEventListener('click', this.onFocusin);
input.elementRef.nativeElement.addEventListener('focusout', this.onFocusout);
input.elementRef.nativeElement.addEventListener('input', this.onInput);
input.elementRef.nativeElement.addEventListener('change', this.onInput);
登入後複製

Листенеры:

  • onFocusin - фосус;
  • onFocusout - блюр;
  • onInput - ввод значения в input.

Также подписываемся на изменение состояния:

   input.ngControl.control?.events
      .pipe(
        filter((event) => event instanceof TouchedChangeEvent),
        tap(() => this.check()),
        takeUntilDestroyed(this.destroyRef),
      )
      .subscribe();

    input.ngControl.valueChanges
      ?.pipe(
        tap(() => {
          if (!input.ngControl.value && this.elementRef.nativeElement.classList.contains('is-value')) {
            this.renderer.removeClass(this.elementRef.nativeElement, 'is-value');
          }
          this.onInput({ target: input.elementRef.nativeElement });
        }),
        takeUntilDestroyed(this.destroyRef),
      )
      .subscribe();

    input.ngControl.statusChanges
      ?.pipe(
        startWith(input.ngControl.status),
        tap((status: FormControlStatus) => {
          this.isDisabled = status === 'DISABLED';
          this.disable();
        }),
        takeUntilDestroyed(this.destroyRef),
      )
      .subscribe();
登入後複製

Autocomplete

Реализуем autocomplete:

mkdir src/app/ui/autocomplete
mkdir src/app/ui/autocomplete/lib
echo >src/app/ui/autocomplete/index.ts
登入後複製

Выполним команду:

yarn ng g c autocomplete
登入後複製

Разметка и стили:

<baf-input-control cdkOverlayOrigin #trigger="cdkOverlayOrigin">
  <label baf-label [attr.for]="options().id">{{ options().label }}</label>
  <input
    #input
    baf-input
    type="text"
    [bafInputDisplay]="options().inputDisplayFn"
    [id]="options().id"
    [formControl]="control()"
    [placeholder]="options().placeholder ?? ''"
    (click)="onOpen()"
    (input)="onInput($event)"
  />
</baf-input-control>

<ng-template
  cdkConnectedOverlay
  [cdkConnectedOverlayOrigin]="trigger"
  [cdkConnectedOverlayOpen]="open()"
  [cdkConnectedOverlayWidth]="width"
  [cdkConnectedOverlayOffsetY]="1"
  (overlayOutsideClick)="onClose()"
>
  <div class="autocomplete-overlay">
    @for (option of data() | async; track option.id; let index = $index) {
      <a class="autocomplete-option" [innerHTML]="options().displayFn(option, index)" (click)="onSelect(option)"></a>
    }
  </div>
</ng-template>
登入後複製
.autocomplete-overlay {
  background-color: var(--md-sys-color-surface-variant);
  color: var(--md-sys-color-on-surface-variant);
  width: 100%;
  padding: 1rem;
  border-radius: 0.25rem;
  box-shadow: 0 2px 5px 0 rgba(0, 0, 0, 0.15);
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
}

.autocomplete-option {
  text-decoration: none;
  padding: 0.5rem;
  color: var(--md-sys-color-on-surface-variant);
  cursor: pointer;

  &:not(:last-child) {
    border-bottom: 1px solid var(--md-sys-color-surface);
  }

  &:hover {
    color: var(--md-sys-color-primary-container);
  }
}
登入後複製

Логика компонента:

import { CdkConnectedOverlay, CdkOverlayOrigin } from '@angular/cdk/overlay';
import { AsyncPipe, NgForOf } from '@angular/common';
import type { Signal } from '@angular/core';
import { ChangeDetectionStrategy, Component, ElementRef, input, output, signal, viewChild } from '@angular/core';
import type { FormControl } from '@angular/forms';
import { ReactiveFormsModule } from '@angular/forms';
import type { Observable } from 'rxjs';
import { take, tap } from 'rxjs';

import type { DisplayFn } from '@baf/core';
import { InputComponent, InputControlComponent, InputDisplayDirective } from '@baf/ui/input';
import { LabelComponent } from '@baf/ui/label';

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type AutocompleteVariant = Record<string, any> & { readonly id: number | string };

export interface AutocompleteOptions {
  readonly label: string;
  readonly placeholder?: string;
  readonly id: string;
  readonly key: string;
  readonly displayFn: DisplayFn;
  readonly inputDisplayFn: DisplayFn;
}

@Component({
  selector: 'baf-autocomplete',
  standalone: true,
  imports: [
    ReactiveFormsModule,
    CdkConnectedOverlay,
    CdkOverlayOrigin,
    InputComponent,
    NgForOf,
    AsyncPipe,
    InputControlComponent,
    InputDisplayDirective,
    LabelComponent,
  ],
  templateUrl: './autocomplete.component.html',
  styleUrl: './autocomplete.component.scss',
  changeDetection: ChangeDetectionStrategy.OnPush,
  host: {
    class: 'baf-input-control',
  },
})
export class AutocompleteComponent {
  readonly control = input.required<FormControl<string | AutocompleteVariant>>();
  readonly options = input.required<AutocompleteOptions>();
  readonly data = input.required<Observable<AutocompleteVariant[]>>();

  readonly changed = output<string>();
  readonly opened = output();
  readonly closed = output();

  readonly input: Signal<ElementRef<HTMLInputElement>> = viewChild.required('input', { read: ElementRef<HTMLInputElement> });

  readonly open = signal<boolean>(false);

  get width(): string {
    return this.input().nativeElement.clientWidth > 200 ? `${this.input().nativeElement.clientWidth}px` : '200px';
  }

  onOpen(): void {
    if (!this.open()) {
      this.open.set(true);
      this.opened.emit();
    }
  }

  onClose(): void {
    this.closed.emit();
    this.open.set(false);

    this.data()
      .pipe(
        take(1),
        tap((options) => {
          if (
            options.length &&
            this.control().value &&
            (typeof this.control().value === 'string' || JSON.stringify(this.control().value) !== JSON.stringify(options[0]))
          ) {
            this.control().patchValue(options[0], { emitEvent: false });
          }
        }),
      )
      .subscribe();
  }

  onInput(event: Event): void {
    this.changed.emit((event.target as HTMLInputElement).value);
  }

  onSelect(option: AutocompleteVariant): void {
    this.control().patchValue(option, { emitEvent: false });
    this.closed.emit();
    this.open.set(false);
  }
}
登入後複製

Суть работы следующая:

  • при клике на поле показать выпадающее окно;
  • при вводе значений, вывести подсказки.

Методы:

  • onOpen - показать окно;
  • onInput - ввод значения;
  • onSelect - выбор подсказки;
  • onClose - событие закрытия.

Показанных компонентов достаточно, чтобы перейти к разработке страниц.

Ссылки

Все исходники находятся на github, в репозитории - github.com/Fafnur/buy-and-fly

Демо можно посмотреть здесь - buy-and-fly.fafn.ru/

Мои группы: telegram, medium, vk, x.com, linkedin, site

以上是Angular 18 中的 UI KIT 開發的詳細內容。更多資訊請關注PHP中文網其他相關文章!

本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn

熱AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover

AI Clothes Remover

用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool

Undress AI Tool

免費脫衣圖片

Clothoff.io

Clothoff.io

AI脫衣器

Video Face Swap

Video Face Swap

使用我們完全免費的人工智慧換臉工具,輕鬆在任何影片中換臉!

熱工具

記事本++7.3.1

記事本++7.3.1

好用且免費的程式碼編輯器

SublimeText3漢化版

SublimeText3漢化版

中文版,非常好用

禪工作室 13.0.1

禪工作室 13.0.1

強大的PHP整合開發環境

Dreamweaver CS6

Dreamweaver CS6

視覺化網頁開發工具

SublimeText3 Mac版

SublimeText3 Mac版

神級程式碼編輯軟體(SublimeText3)

熱門話題

Java教學
1677
14
CakePHP 教程
1431
52
Laravel 教程
1334
25
PHP教程
1279
29
C# 教程
1257
24
Python vs. JavaScript:學習曲線和易用性 Python vs. JavaScript:學習曲線和易用性 Apr 16, 2025 am 12:12 AM

Python更適合初學者,學習曲線平緩,語法簡潔;JavaScript適合前端開發,學習曲線較陡,語法靈活。 1.Python語法直觀,適用於數據科學和後端開發。 2.JavaScript靈活,廣泛用於前端和服務器端編程。

JavaScript和Web:核心功能和用例 JavaScript和Web:核心功能和用例 Apr 18, 2025 am 12:19 AM

JavaScript在Web開發中的主要用途包括客戶端交互、表單驗證和異步通信。 1)通過DOM操作實現動態內容更新和用戶交互;2)在用戶提交數據前進行客戶端驗證,提高用戶體驗;3)通過AJAX技術實現與服務器的無刷新通信。

JavaScript在行動中:現實世界中的示例和項目 JavaScript在行動中:現實世界中的示例和項目 Apr 19, 2025 am 12:13 AM

JavaScript在現實世界中的應用包括前端和後端開發。 1)通過構建TODO列表應用展示前端應用,涉及DOM操作和事件處理。 2)通過Node.js和Express構建RESTfulAPI展示後端應用。

了解JavaScript引擎:實施詳細信息 了解JavaScript引擎:實施詳細信息 Apr 17, 2025 am 12:05 AM

理解JavaScript引擎內部工作原理對開發者重要,因為它能幫助編寫更高效的代碼並理解性能瓶頸和優化策略。 1)引擎的工作流程包括解析、編譯和執行三個階段;2)執行過程中,引擎會進行動態優化,如內聯緩存和隱藏類;3)最佳實踐包括避免全局變量、優化循環、使用const和let,以及避免過度使用閉包。

Python vs. JavaScript:開發環境和工具 Python vs. JavaScript:開發環境和工具 Apr 26, 2025 am 12:09 AM

Python和JavaScript在開發環境上的選擇都很重要。 1)Python的開發環境包括PyCharm、JupyterNotebook和Anaconda,適合數據科學和快速原型開發。 2)JavaScript的開發環境包括Node.js、VSCode和Webpack,適用於前端和後端開發。根據項目需求選擇合適的工具可以提高開發效率和項目成功率。

C/C在JavaScript口譯員和編譯器中的作用 C/C在JavaScript口譯員和編譯器中的作用 Apr 20, 2025 am 12:01 AM

C和C 在JavaScript引擎中扮演了至关重要的角色,主要用于实现解释器和JIT编译器。1)C 用于解析JavaScript源码并生成抽象语法树。2)C 负责生成和执行字节码。3)C 实现JIT编译器,在运行时优化和编译热点代码,显著提高JavaScript的执行效率。

從網站到應用程序:JavaScript的不同應用 從網站到應用程序:JavaScript的不同應用 Apr 22, 2025 am 12:02 AM

JavaScript在網站、移動應用、桌面應用和服務器端編程中均有廣泛應用。 1)在網站開發中,JavaScript與HTML、CSS一起操作DOM,實現動態效果,並支持如jQuery、React等框架。 2)通過ReactNative和Ionic,JavaScript用於開發跨平台移動應用。 3)Electron框架使JavaScript能構建桌面應用。 4)Node.js讓JavaScript在服務器端運行,支持高並發請求。

Python vs. JavaScript:比較用例和應用程序 Python vs. JavaScript:比較用例和應用程序 Apr 21, 2025 am 12:01 AM

Python更適合數據科學和自動化,JavaScript更適合前端和全棧開發。 1.Python在數據科學和機器學習中表現出色,使用NumPy、Pandas等庫進行數據處理和建模。 2.Python在自動化和腳本編寫方面簡潔高效。 3.JavaScript在前端開發中不可或缺,用於構建動態網頁和單頁面應用。 4.JavaScript通過Node.js在後端開發中發揮作用,支持全棧開發。

See all articles