- pnpm dlx @angular/cli@latest new APP_NAME -s -t --routing --package-manager=pnpm --ssr
- => Add
--experimental-zoneless
for zoneless change detection - =>
-s means --inline-style
-t means --inline-template
- pnpm add -D @spartan-ng/cli
- pnpm add @angular/cdk @spartan-ng/ui-core
- pnpm add -D tailwindcss@3 postcss autoprefixer
- pnpm dlx tailwindcss init
tailwind.config.js
/** @type {import('tailwindcss').Config} */
module.exports = {
presets: [require('@spartan-ng/ui-core/hlm-tailwind-preset')],
content: [
'./src/**/*.{html,ts}',
'./REPLACE_WITH_PATH_TO_YOUR_COMPONENTS_DIRECTORY/**/*.{html,ts}',
],
theme: {
extend: {},
},
plugins: [],
};
REPLACE_WITH_PATH_TO_YOUR_COMPONENTS_DIRECTORY => libs/ui
styles.css
@tailwind base;
@tailwind components;
@tailwind utilities;
- pnpm ng g @spartan-ng/cli:ui-theme
- pnpm ng g @spartan-ng/cli:ui
- pnpm ng add ngxtension
- pnpm add @tanstack/angular-table @tanstack/angular-query-experimental
- pnpm add @tanstack/angular-form @tanstack/zod-form-adapter zod
Final style.css
@import "@angular/cdk/overlay-prebuilt.css";
@tailwind base;
@tailwind components;
@tailwind utilities;
:root {
--font-sans: "Inter";
}
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--muted: 210 40% 96.1%;
--muted-foreground: 215.4 16.3% 46.9%;
--popover: 0 0% 100%;
--popover-foreground: 222.2 84% 4.9%;
--card: 0 0% 100%;
--card-foreground: 222.2 84% 4.9%;
--border: 214.3 31.8% 91.4%;
--input: 214.3 31.8% 91.4%;
--primary: 222.2 47.4% 11.2%;
--primary-foreground: 210 40% 98%;
--secondary: 210 40% 96.1%;
--secondary-foreground: 222.2 47.4% 11.2%;
--accent: 210 40% 96.1%;
--accent-foreground: 222.2 47.4% 11.2%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 210 40% 98%;
--ring: 222.2 84% 4.9%;
--radius: 0.5rem;
color-scheme: light;
}
.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
--muted: 217.2 32.6% 17.5%;
--muted-foreground: 215 20.2% 65.1%;
--popover: 222.2 84% 4.9%;
--popover-foreground: 210 40% 98%;
--card: 222.2 84% 4.9%;
--card-foreground: 210 40% 98%;
--border: 217.2 32.6% 17.5%;
--input: 217.2 32.6% 17.5%;
--primary: 210 40% 98%;
--primary-foreground: 222.2 47.4% 11.2%;
--secondary: 217.2 32.6% 17.5%;
--secondary-foreground: 210 40% 98%;
--accent: 217.2 32.6% 17.5%;
--accent-foreground: 210 40% 98%;
--destructive: 0, 91%, 71%;
--destructive-foreground: 210 40% 98%;
--ring: 212.7 26.8% 83.9;
color-scheme: dark;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}
angular.json Add necessary settings
"schematics": {
"@schematics/angular:component": {
"inlineTemplate": true,
"inlineStyle": true,
"standalone": true,
"changeDetection": "OnPush"
}
}
- pnpm ng add @angular-eslint/schematics
package.json
scripts: {
"lint": "ng lint",
"lint:fix": "ng lint --fix",
"prettier:check": "prettier --check ./src",
"prettier:write": "prettier --write ./src"
}
eslint.config.js
// @ts-check
const eslint = require('@eslint/js');
const tseslint = require('typescript-eslint');
const angular = require('angular-eslint');
module.exports = tseslint.config(
{
files: ['**/*.ts'],
extends: [
eslint.configs.recommended,
...tseslint.configs.recommended,
...tseslint.configs.stylistic,
...angular.configs.tsRecommended,
],
processor: angular.processInlineTemplates,
rules: {
'@angular-eslint/directive-selector': [
'error',
{
type: 'attribute',
prefix: 'app',
style: 'camelCase',
},
],
'@angular-eslint/component-selector': [
'error',
{
type: 'element',
prefix: 'app',
style: 'kebab-case',
},
],
'@typescript-eslint/no-unused-vars': ['warn'],
},
},
{
files: ['**/*.html'],
extends: [
...angular.configs.templateRecommended,
...angular.configs.templateAccessibility,
],
rules: {
'prettier/prettier': [
'error',
{
parser: 'angular',
},
],
},
}
);
-
pnpm add -D prettier@latest
-
touch .prettierignore
# See http://help.github.com/ignore-files/ for more about ignoring files.
# Compiled output
/dist
/tmp
/out-tsc
/bazel-out
# Node
/node_modules
npm-debug.log
yarn-error.log
# IDEs and editors
.idea/
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# Visual Studio Code
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
.history/*
# Miscellaneous
/.angular/cache
.sass-cache/
/connect.lock
/coverage
/libpeerconnection.log
testem.log
/typings
# System files
.DS_Store
Thumbs.db
libs/ui
- touch .prettierrc.json
{
"tabWidth": 2,
"useTabs": false,
"singleQuote": true,
"semi": true,
"bracketSpacing": true,
"arrowParens": "avoid",
"trailingComma": "es5",
"bracketSameLine": true,
"printWidth": 80,
"endOfLine": "lf"
}
app.component.ts - and change detection strategy
@Component({
selector: 'app-root',
standalone: true,
imports: [ RouterOutlet ],
host: {
class: ''
},
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<router-outlet />
`,
styles: [],
})
index.html - add script and google font to choose theme in <head>
tag
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap" rel="stylesheet">
<script>
if (
// check if user had saved dark as their
// theme when accessing page before
localStorage.theme === "dark" ||
// or user's requesting dark color
// scheme through operating system
(!("theme" in localStorage) &&
window.matchMedia("(prefers-color-scheme: dark)").matches)
) {
// then if we have access to the document and the element
// we add the dark class to the html element and
// store the dark value in the localStorage
if (document && document.documentElement) {
document.documentElement.classList.add("dark");
localStorage.setItem("theme", "dark");
}
} else {
// else if we have access to the document and the element
// we remove the dark class to the html element and
// store the value light in the localStorage
if (document && document.documentElement) {
document.documentElement.classList.remove("dark");
localStorage.setItem("theme", "light");
}
}
</script>
tsconfig.json
"paths": [
"@/*": ["./src/app/*"],
"@spartan-ng/ui-accordion-helm": [
"./libs/ui/ui-accordion-helm/src/index.ts"
],
//....... others use ./
]
theme.type.ts Type for the theme
export type Theme = 'light' | 'dark';
theme.service.ts - Theme service
import { DOCUMENT, isPlatformBrowser } from '@angular/common';
import { injectDestroy } from 'ngxtension/inject-destroy';
import {
Injectable,
PLATFORM_ID,
RendererFactory2,
inject,
} from '@angular/core';
import { ReplaySubject, takeUntil } from 'rxjs';
import { Theme } from '@/types/theme.type';
@Injectable({
providedIn: 'root',
})
export class ThemeService {
private readonly THEME = 'theme';
private readonly _platformId = inject(PLATFORM_ID);
private readonly _renderer = inject(RendererFactory2).createRenderer(
null,
null
);
private readonly _document = inject(DOCUMENT);
private readonly _destroy$ = injectDestroy();
private _theme = new ReplaySubject<Theme>(1);
public theme$ = this._theme.asObservable();
constructor() {
this._syncThemeFromLocalStorage();
this._toggleClassOnThemeChanges();
}
private _syncThemeFromLocalStorage(): void {
if (isPlatformBrowser(this._platformId)) {
this._theme.next(
localStorage.getItem(this.THEME) === 'dark' ? 'dark' : 'light'
);
}
}
private _toggleClassOnThemeChanges(): void {
this.theme$.pipe(takeUntil(this._destroy$)).subscribe(theme => {
if (theme === 'dark') {
this._renderer.addClass(this._document.documentElement, 'dark');
} else {
if (this._document.documentElement.className.includes('dark')) {
this._renderer.removeClass(this._document.documentElement, 'dark');
}
}
});
}
public setTheme(theme: Theme) {
localStorage.setItem(this.THEME, theme);
this._theme.next(theme);
}
}
Generate environments
pnpm ng g environments
Generate tailwind Indicator
pnpm ng g c components/tailwind-indicator -s -t --skip-tests
tailwind-indicator.component.ts
import { ChangeDetectionStrategy, Component, isDevMode } from '@angular/core';
@Component({
selector: 'app-tailwind-indicator',
standalone: true,
imports: [],
template: `
@if (_isDevMode) {
<div
class="fixed bottom-1 left-1 z-50 flex h-6 w-6 items-center justify-center rounded-full bg-gray-800 p-3 text-xs text-white">
<div class="block sm:hidden" title="xs: <=640px">xs</div>
<div class="hidden sm:block md:hidden" title="sm: >=640px && <768px">
sm
</div>
<div class="hidden md:block lg:hidden" title="md: >=768px && <1024px">
md
</div>
<div class="hidden lg:block xl:hidden" title="lg: >=1024px && <1280px">
lg
</div>
<div class="hidden xl:block 2xl:hidden" title="xl: >=1280px && <1536px">
xl
</div>
<div class="hidden 2xl:block" title="2xl: >=1536px">2xl</div>
</div>
}
`,
styles: ``,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class TailwindIndicatorComponent {
protected readonly _isDevMode = isDevMode();
}
Generate tailwind Indicator
pnpm ng g c components/network-state -s -t --skip-tests
network-state.component.ts
import {
ChangeDetectionStrategy,
Component,
computed,
effect,
signal,
} from '@angular/core';
import { injectNetwork } from 'ngxtension/inject-network';
@Component({
selector: 'app-network-state',
standalone: true,
imports: [],
host: {
class: 'fixed bottom-0 w-full z-[100] bg-background',
},
template: `
@if (_networkState.online() === false) {
<p
class="bg-rose-500 text-background dark:bg-rose-400 dark:text-foreground text-center text-sm font-semibold py-1">
You are offline since {{ offlineAt() }}
</p>
} @else {
@if (_showOnlineMessage() === true) {
<p
class="bg-green-500 text-background dark:bg-green-600 dark:text-foreground text-center text-sm font-semibold py-1">
Back to online
</p>
}
}
`,
styles: ``,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class NetworkStateComponent {
protected _networkState = injectNetwork();
protected _showOnlineMessage = signal(false);
constructor() {
effect(() => {
if (this._networkState.onlineAt() && this._networkState.offlineAt()) {
this._showOnlineMessage.set(true);
setTimeout(() => {
this._showOnlineMessage.set(false);
}, 3000);
}
});
}
protected offlineAt = computed(() => {
const time = this._networkState.offlineAt();
if (!time) {
return;
}
return new Date(time).toLocaleTimeString();
});
}