Skip to content

Releases: swifweb/web

⭐️ Meta: add `property` attribute by @tierracero

20 Sep 02:14
3716a4b
Compare
Choose a tag to compare

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

31 Mar 17:26
Compare
Choose a tag to compare

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)

30 Mar 03:46
Compare
Choose a tag to compare
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

28 Mar 16:54
Compare
Choose a tag to compare
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

14 Mar 00:36
Compare
Choose a tag to compare

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`

14 Jan 17:58
Compare
Choose a tag to compare

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

26 Dec 12:50
Compare
Choose a tag to compare

🖥 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

12 Dec 10:11
2a7b711
Compare
Choose a tag to compare

🔑 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

04 Dec 13:44
Compare
Choose a tag to compare
@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...
}

🪚 Improve `@media` rule syntax

04 Dec 13:40
Compare
Choose a tag to compare
@main
public class App: WebApp {
    @AppBuilder public override var body: AppBuilder.Content {
        /// ...
        MainStyle()
    }
}

class MainStyle: Stylesheet {
    @Rules override var rules: Rules.Content {
        MediaRule(.screen.maxWidth(800.px)) {
            Rule(Body.pointer)
                .backgroundColor(.red)
        }
        MediaRule(.screen.maxWidth(1200.px)) {
            Rule(Body.pointer)
                .backgroundColor(.green)
        }
        MediaRule(.screen.aspectRatio(4/3)) {
            Rule(Body.pointer)
                .backgroundColor(.purple)
        }
        MediaRule(!.screen.aspectRatio(4/3)) {
            Rule(Body.pointer)
                .backgroundColor(.black)
        }
    }
}

which represents

@media only screen and (max-width: 800px) {
    background-color: red;
}
@media only screen and (max-width: 1200px) {
    background-color: green;
}
@media only screen and (aspect-ratio: 4/3) {
    background-color: purple;
}
@media not screen and (aspect-ratio: 4/3) {
    background-color: black;
}