Releases: swifweb/web
🚀 Stable v1
🥲 After so many years in beta
it is time to mark it as stable v1
📚 Full documentation for v1
is in the Hackernoon article
🔭 This is the start point for v2
development which will bring the new more convenient development experience
🌙 Stay tuned and track the nightly
branch
⭐️ Meta: add `property` attribute by @tierracero
The Open Graph protocol enables any web page to become a rich object in a social graph.
This kind of meta tags
<meta property="og:title" content="The Rock" />
<meta property="og:type" content="video.movie" />
<meta property="og:url" content="https://www.imdb.com/title/tt0117500/" />
<meta property="og:image" content="https://ia.media-imdb.com/images/rock.jpg" />
Now could be declared the following way
Meta().property("og:title").content("The Rock")
Meta().property("og:type").content("video.movie")
Meta().property("og:url").content("https://www.imdb.com/title/tt0117500/")
Meta().property("og:image").content("https://ia.media-imdb.com/images/rock.jpg")
🖥 `DOM.print` method
Normally you can't use print
inside of the @DOM
since it is a function builder which takes DOM elements, it is not regular function
@DOM override var body: DOM.Content {
// you can't use print statements here, it is not regular function
// if/else, if let, guard let statements are not like in regular functions
}
But now it is possible to print
inside of the @DOM
this way
let hello: String? = nil
@DOM override var body: DOM.Content {
if let hello = self.hello {
DOM.print("hello is not null: \(hello)")
} else {
DOM.print("hello is null")
}
}
🛠 FetchAPI: fix `RequestOptions` (bonus: GraphQL example)
let options = RequestOptions()
options.method(.post)
options.header("Content-Type", "application/json")
struct ExecutionArgs: Encodable {
let query: String
let variables: [String: String]
}
do {
let jsonData = try JSONEncoder().encode(ExecutionArgs(query: """
query {
ships {
name
model
}
}
""", variables: ["name": "users"]))
if let jsonString = String(data: jsonData, encoding: .utf8) {
options.body(jsonString)
} else {
print("🆘 Unable to encode body")
}
} catch {
print("🆘 Something went wrong: \(error)")
}
Fetch("https://spacex-production.up.railway.app/", options) { result in
switch result {
case .failure:
break
case .success(let response):
guard response.ok else {
print("🆘 Response status code is: \(response.status)")
return
}
struct Response: Decodable {
struct Data: Decodable {
struct Ship: Decodable {
let name: String
let model: String?
}
let ships: [Ship]
}
let data: Data
}
response.json(as: Response.self) { result in
switch result {
case .failure(let error):
print("🆘 Unable to decode response: \(error)")
case .success(let response):
print("✅ Ships: \(response.data.ships.map { $0.name }.joined(separator: ", "))")
}
}
break
}
}
🚦 Improved nested routing
import Web
@main
class App: WebApp {
@AppBuilder override var app: Configuration {
Routes {
Page { IndexPage() }
Page("space") { SpacePage() }
Page("**") { NotFoundPage() }
}
}
}
class SpacePage: PageController {
// here we pass all fragment routes into the root router
class override var fragmentRoutes: [FragmentRoutes] { [fragment] }
// here we declare fragment with its relative routes
static var fragment = FragmentRoutes {
Page("earth") {
PageController { "earth" }.onDidLoad {
print("🌎 earth loaded")
}
}
Page("moon") {
PageController { "moon" }.onDidLoad {
print("🌙 moon loaded")
}
}
}
// you can declare multiple different fragment routes
@DOM override var body: DOM.Content {
H1("Space Page")
Button("Load Earth").display(.block).onClick {
self.changePath(to: "/space/earth")
}
Br()
Button("Load Moon").display(.block).onClick {
self.changePath(to: "/space/moon")
}
FragmentRouter(self, Self.fragment) // <== here we add fragment into the DOM
}
}
🚦 Nested routing, page controller lifecycle, and more
FragmentRouter
We may not want to replace the entire content on the page for the next route, but only certain blocks.
This is where the new FragmentRouter
comes in handy!
Let's consider that we have tabs on the /user
page. Each tab is a subroute, and we want to react to changes in the subroute using the FragmentRouter
without reloading use page even though url changes.
Declare the top-level route in the App
class
Page("user") { UserPage() }
And declare FragmentRouter
in the UserPage
class
class UserPage: PageController {
@DOM override var body: DOM.Content {
// NavBar is from Materialize library :)
Navbar()
.item("Profile") { self.changePath(to: "/user/profile") }
.item("Friends") { self.changePath(to: "/user/friends") }
FragmentRouter(self)
.routes {
Page("profile") { UserProfilePage() }
Page("friends") { UserFriendsPage() }
}
}
}
In the example above FragmentRouter
handles /user/profile
and /user/friends
subroutes and renders it under the Navbar
, so page never reload the whole content but only specific fragments. There are also may be declared more than one fragment with the same or different subroutes and they all will just work together like a magic!
Btw FragmentRouter
is a Div
and you may configure it by calling
FragmentRouter(self)
.configure { div in
// do anything you want with the div
}
Breaking changes
ViewController
has been renamed into PageController
, Xcode will propose to rename it automatically.
PageController
PageController
now have lifecycle methods: willLoad
, didLoad
, willUnload
, didUnload
.
override func willLoad(with req: PageRequest) {
super.willLoad(with: req)
}
override func didLoad(with req: PageRequest) {
super.didLoad(with: req)
// set page title and metaDescription
// also parse query and hash
}
override func willUnload() {
super.willUnload()
}
override func didUnload() {
super.didUnload()
}
Also you can declare same methods without overriding, e.g. when you declare little page without subclassing
PageController { page in
H1("Hello world")
P("Text under title")
Button("Click me") {
page.alert("Click!")
print("button clicked")
}
}
.backgroundcolor(.lightGrey)
.onWillLoad { page in }
.onDidLoad { page in }
.onWillUnload { page in }
.onDidUnload { page in }
New convenience methods
alert(message: String)
- direct JS alert method
changePath(to: String)
- switching URL path
More
Id
and Class
now can be initialized simply with string like this
Class("myClass")
Id("myId")
Tiny little change but may be very useful.
App.current.window.document.querySelectorAll("your_query")
now works!
Tip
🚨Please don't forget to update Webber CLI
tool to version 1.6.1
or above!
🫶 `ForEach` for `DOM` and `CSS`
DOM
Static example
let names = ["Bob", "John", "Annie"]
ForEach(names) { name in
Div(name)
}
// or
ForEach(names) { index, name in
Div("\(index). \(name)")
}
Dynamic example
@State var names = ["Bob", "John", "Annie"]
ForEach($names) { name in
Div(name)
}
// or with index
ForEach($names) { index, name in
Div("\(index). \(name)")
}
Button("Change 1").onClick {
self.names.append("George") // this will append new Div with name automatically
}
Button("Change 2").onClick {
self.names = ["Bob", "Peppa", "George"] // this will replace and update Divs with names automatically
}
It is also easy to use it with ranges
ForEach(1...20) { index in
Div()
}
And even simpler to place X-times same element on the screen
20.times {
Div().class(.shootingStar)
}
CSS
Same as in examples above, but also BuilderFunction
is available
Stylesheet {
ForEach(1...20) { index in
CSSRule(Div.pointer.nthChild("\(index)"))
// set rule properties depending on index
}
20.times { index in
CSSRule(Div.pointer.nthChild("\(index)"))
// set rule properties depending on index
}
}
BuilderFunction
You can use BuilderFunction
in ForEach
loops to calculate some value one time only like a delay
value in the following example
ForEach(1...20) { index in
BuilderFunction(9999.asRandomMax()) { delay in
CSSRule(Pointer(".shooting_star").nthChild("\(index)"))
.custom("top", "calc(50% - (\(400.asRandomMax() - 200)px))")
.custom("left", "calc(50% - (\(300.asRandomMax() + 300)px))")
.animationDelay(delay.ms)
CSSRule(Pointer(".shooting_star").nthChild("\(index)").before)
.animationDelay(delay.ms)
CSSRule(Pointer(".shooting_star").nthChild("\(index)").after)
.animationDelay(delay.ms)
}
}
it can also take function as an argument
BuilderFunction({ return 1 + 1 }) { calculatedValue in
// CSS rule or DOM element
}
LivePreview, DOM, and CSS improvements
🖥 Improve LivePreview
declaration
Old way
class Index_Preview: WebPreview {
override class var language: Language { .en }
override class var title: String { "Index page" }
override class var width: UInt { 600 }
override class var height: UInt { 480 }
@Preview override class var content: Preview.Content {
AppStyles.all
IndexPage()
}
}
New way
class Index_Preview: WebPreview {
@Preview override class var content: Preview.Content {
Language.en
Title("Index page")
Size(600, 480)
AppStyles.all
IndexPage()
}
}
🪚 DOM: make attribute
method public
Now you can set custom attributes or not-supported attributes simply by calling
Div()
.attribute("my-custom-attribute", "myCustomValue")
🎨 Fix CSS properties
Stop color for gradients now can be set these ways
// convenient way
.red.stop(80) // red / 80%
// short way
.red/80 // red / 80%
BackgroundClipType
got new text
value
Fixed properties with browser prefixes, now they all work as expected
BackgroundImageProperty
got dedicated initializer with CSSFunction
Fix uid generation, CSS `!important` modifier, multiple classes
🔑 Fix uid generation
Excluded digits from the uid cause css doesn't allow ids which starts with digit.
🪚 Class
stores multiple names
Now you can instantiate Class
with multiple values like .class("one", "two", "three")
🎨 CSS: implement !important
modifier
Yeah, that modifier is very important 😀
// simple values can just call `.important` in the end, e.g.:
.backgroundColor(.white.important)
.height(100.px.important)
.width(100.percent.important)
.display(.block.important)
// all complex calls now have `important: Bool` parameter, e.g.:
.border(width: .length(1.px), style: .solid, color: .white, important: true)
.backgroundColor(r: 255, g: 255, b: 255, a: 0.26, important: true)
.transition(.property(.backgroundColor), duration: .seconds(0.3), timingFunction: .easeIn, important: true)
🪚 Allow to put `Style` into `@DOM` block
@DOM override var body: DOM.Content {
Stylesheet {
Rule(Body.pointer)
.margin(all: 0.px)
.padding(all: 0.px)
MediaRule(.all.maxWidth(800.px)) {
Rule(Body.pointer)
.backgroundColor(0x9bc4e2)
}
MediaRule(.all.maxWidth(1200.px)) {
Rule(Body.pointer)
.backgroundColor(0xffd700)
}
}
// ...other elements...
}